이벤트 가져오기 위젯 테스트 추가 및 테스트 훅 도입
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/screens/club/club_settings_screen.dart';
|
||||
import 'package:lanebow/models/club_model.dart';
|
||||
import 'package:lanebow/models/member_model.dart';
|
||||
import 'package:lanebow/models/user_model.dart';
|
||||
import '../../utils/mock_services.dart';
|
||||
|
||||
void main() {
|
||||
Widget wrapWithProviders({required List<Member> members, required ValueChanged<int> onBuilt}) {
|
||||
final club = Club(id: 'c1', name: '테스트클럽', ownerId: 'u1');
|
||||
final user = User(id: 'u1', email: 'a@a.com', name: 'Admin', role: 'admin');
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (_) => MockAuthService(user: user)),
|
||||
ChangeNotifierProvider(create: (_) => MockClubService(club: club)),
|
||||
ChangeNotifierProvider(create: (_) => MockMemberService(seed: members)),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ClubSettingsScreen(
|
||||
onOwnerItemsBuilt: onBuilt,
|
||||
initialMembers: members,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
group('ClubSettingsScreen - owner dropdown items', () {
|
||||
testWidgets('reports count when members exist', (tester) async {
|
||||
final seed = [
|
||||
Member(id: 'm1', userId: 'u1', clubId: 'c1', name: 'Alice', email: 'a@a.com', isActive: true, memberType: 'owner'),
|
||||
Member(id: 'm2', userId: 'u2', clubId: 'c1', name: 'Bob', email: 'b@b.com', isActive: true, memberType: 'manager'),
|
||||
];
|
||||
final completer = Completer<int>();
|
||||
|
||||
await tester.pumpWidget(wrapWithProviders(
|
||||
members: seed,
|
||||
onBuilt: (c) => completer.complete(c),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
final count = await completer.future.timeout(const Duration(seconds: 1));
|
||||
expect(count, 2);
|
||||
});
|
||||
|
||||
testWidgets('reports zero when members list is empty', (tester) async {
|
||||
final completer = Completer<int>();
|
||||
|
||||
await tester.pumpWidget(wrapWithProviders(
|
||||
members: const [],
|
||||
onBuilt: (c) => completer.complete(c),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
final count = await completer.future.timeout(const Duration(seconds: 1));
|
||||
expect(count, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/models/event_model.dart';
|
||||
import 'package:lanebow/models/club_model.dart';
|
||||
import 'package:lanebow/screens/club/event_details_screen.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/services/club_service.dart';
|
||||
import 'package:lanebow/services/member_service.dart';
|
||||
|
||||
import '../../utils/mock_services.dart';
|
||||
|
||||
import '../../utils/stub_event_service.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('EventDetailsScreen AppBar duplicate action clones and pops with true', (tester) async {
|
||||
// Arrange
|
||||
final stub = StubEventService();
|
||||
final event = Event(
|
||||
id: 'e1',
|
||||
clubId: 'club1',
|
||||
title: '테스트 이벤트',
|
||||
startDate: DateTime(2025, 1, 1),
|
||||
status: 'active',
|
||||
isActive: true,
|
||||
);
|
||||
|
||||
final mockClub = MockClubService(club: Club(
|
||||
id: 'club1',
|
||||
name: '클럽',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
));
|
||||
final mockMembers = MockMemberService(seed: const []);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EventService>.value(value: stub),
|
||||
ChangeNotifierProvider<ClubService>.value(value: mockClub),
|
||||
ChangeNotifierProvider<MemberService>.value(value: mockMembers),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: EventDetailsScreen(event: event),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Act: tap duplicate icon button on AppBar
|
||||
final dupBtn = find.byIcon(Icons.copy);
|
||||
expect(dupBtn, findsOneWidget);
|
||||
await tester.tap(dupBtn);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert: cloneEvent invoked on service
|
||||
expect(stub.lastCloneCalled, isTrue);
|
||||
expect(stub.lastClonedEventId, equals('e1'));
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lanebow/models/event_model.dart';
|
||||
@@ -6,11 +7,14 @@ import 'package:lanebow/models/participant_model.dart';
|
||||
import 'package:lanebow/models/score_model.dart';
|
||||
import 'package:lanebow/models/team_model.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/services/event_bus.dart';
|
||||
import '../../utils/event_bus_test_utils.dart';
|
||||
import 'package:lanebow/utils/test_utils.dart';
|
||||
import 'package:lanebow/screens/club/team_generator_screen.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'test_event_details_screen.dart';
|
||||
import '../../utils/dummy_test_data.dart';
|
||||
|
||||
// 이벤트 서비스 모킹을 위한 어노테이션
|
||||
@GenerateMocks([EventService])
|
||||
@@ -23,14 +27,24 @@ void main() {
|
||||
// 클립보드 및 플러그인 모킹 설정
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 클립보드 모킹
|
||||
const MethodChannel('plugins.flutter.io/clipboard')
|
||||
.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
// 클립보드 모킹 (deprecated API 대체)
|
||||
final channel = const MethodChannel('plugins.flutter.io/clipboard');
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (MethodCall methodCall) async {
|
||||
if (methodCall.method == 'setData') {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 탭 컨트롤러 오류 무시
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
@@ -43,29 +57,18 @@ void main() {
|
||||
FlutterError.presentError(details);
|
||||
};
|
||||
|
||||
setUp(() {
|
||||
// EventBus 초기화
|
||||
EventBus().reset();
|
||||
|
||||
setUp(() async {
|
||||
// EventBus 테스트 환경 초기화 (플래키 방지)
|
||||
TestUtils.setTestMode(true);
|
||||
await EventBusTestUtils.setUp('event_details_screen_test');
|
||||
|
||||
mockEventService = MockEventService();
|
||||
|
||||
// 테스트용 이벤트 데이터 생성
|
||||
testEvent = Event(
|
||||
|
||||
// 테스트용 이벤트 데이터 생성 (공용 더미 유틸 활용)
|
||||
testEvent = DummyTestData.event(
|
||||
id: '1',
|
||||
clubId: '1',
|
||||
title: '테스트 이벤트',
|
||||
description: '테스트 설명',
|
||||
type: '팀전', // 팀전으로 변경하여 팀 탭이 표시되도록 설정
|
||||
status: '예정',
|
||||
startDate: DateTime.now(),
|
||||
endDate: DateTime.now().add(const Duration(hours: 3)),
|
||||
location: '테스트 볼링장',
|
||||
maxParticipants: 20,
|
||||
gameCount: 3,
|
||||
participantFee: 10000,
|
||||
registrationDeadline: DateTime.now().add(const Duration(days: 1)),
|
||||
publicHash: 'test123',
|
||||
accessPassword: 'pass123',
|
||||
isActive: true,
|
||||
);
|
||||
|
||||
@@ -79,9 +82,9 @@ void main() {
|
||||
.thenAnswer((_) async => <Team>[Team(id: '1', eventId: '1', name: '팀 1', memberIds: [])]);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
// 테스트 종료 후 EventBus 초기화
|
||||
EventBus().reset();
|
||||
tearDown(() async {
|
||||
// 테스트 종료 후 EventBus 자원 정리 (idle/타이머 플러시 포함)
|
||||
await EventBusTestUtils.tearDown();
|
||||
});
|
||||
|
||||
// 스크롤 도우미 함수
|
||||
@@ -124,6 +127,47 @@ void main() {
|
||||
expect(find.text('팀').first, findsOneWidget); // 팀 탭
|
||||
});
|
||||
|
||||
testWidgets('참가자 탭: 404 오류 발생 시 전용 문구 표시 및 재시도 성공 시 해제', (WidgetTester tester) async {
|
||||
// given: 최초 드롭다운 변경 시 404 예외 모킹
|
||||
when(mockEventService.fetchEventParticipants(any)).thenThrow(Exception('HTTP 404 Not Found'));
|
||||
|
||||
final dummyParticipants = <Participant>[
|
||||
Participant(id: 'p1', eventId: '1', memberId: 'm1', name: '홍길동'),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: dummyParticipants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭 이동 → 드롭다운에서 '취소' 선택하여 실패 유도
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dropdown = find.byKey(const Key('participant_filter_dropdown'));
|
||||
expect(dropdown, findsOneWidget);
|
||||
await tester.tap(dropdown);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('취소').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 404 전용 에러 문구 + 재시도 버튼
|
||||
expect(find.byKey(const Key('participant_error_text')), findsOneWidget);
|
||||
expect(find.text('참가자 정보가 없습니다 (404)'), findsOneWidget);
|
||||
expect(find.byKey(const Key('participant_retry_button')), findsOneWidget);
|
||||
|
||||
// given: 재시도는 성공 모킹
|
||||
final retryCompleter = Completer<List<Participant>>();
|
||||
when(mockEventService.fetchEventParticipants(any)).thenAnswer((_) => retryCompleter.future);
|
||||
|
||||
// when: 재시도 수행
|
||||
await tester.tap(find.byKey(const Key('participant_retry_button')));
|
||||
await tester.pump();
|
||||
retryCompleter.complete(<Participant>[]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 에러 문구 해제
|
||||
expect(find.byKey(const Key('participant_error_text')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('이벤트 유형 및 상태 뱃지가 올바르게 표시되어야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen());
|
||||
@@ -217,106 +261,208 @@ void main() {
|
||||
expect(find.text('참가 링크 포함 공유'), findsOneWidget);
|
||||
expect(find.text('상세 정보 포함 공유'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('핸디캡 토글 시 총점 라벨이 올바르게 변경되어야 함', (WidgetTester tester) async {
|
||||
// given: 핸디캡이 포함된 단일 점수 데이터
|
||||
final score = Score(
|
||||
id: 's1',
|
||||
memberId: 'm1',
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
frames: const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
totalScore: 100,
|
||||
handicap: 10,
|
||||
date: DateTime(2024, 1, 1),
|
||||
participantName: '홍길동',
|
||||
);
|
||||
final participant = Participant(
|
||||
id: 'p1',
|
||||
eventId: '1',
|
||||
memberId: 'm1',
|
||||
name: '홍길동',
|
||||
status: 'confirmed',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(
|
||||
scores: [score],
|
||||
participants: [participant],
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 점수 탭으로 이동
|
||||
await tester.tap(find.text('점수').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 초기(핸디캡 미포함) 총점 라벨 확인
|
||||
expect(find.textContaining('총점: 100'), findsOneWidget);
|
||||
expect(find.textContaining('(100+10)'), findsNothing);
|
||||
|
||||
// when: 핸디캡 스위치 켜기
|
||||
await tester.tap(find.byType(Switch));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 핸디캡 포함 총점 라벨로 변경
|
||||
expect(find.textContaining('총점: 110 (100+10)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('점수 탭: 새로고침 중 로딩 오버레이 표시, 완료 후 해제 및 스낵바 표시', (WidgetTester tester) async {
|
||||
// given: fetchEventScores 지연 설정 + 초기 더미 점수/참가자 주입(리스트/컨트롤 렌더링 보장)
|
||||
final completer = Completer<List<Score>>();
|
||||
when(mockEventService.fetchEventScores(any)).thenAnswer((_) => completer.future);
|
||||
|
||||
final dummyScores = <Score>[
|
||||
Score(
|
||||
id: 's1', memberId: 'm1', eventId: '1', clubId: '1',
|
||||
frames: const [10, 9, 1, 8, 2, 7, 3, 6, 4, 5], totalScore: 150,
|
||||
date: DateTime(2024, 1, 1), participantName: '홍길동',
|
||||
),
|
||||
];
|
||||
final dummyParticipants = <Participant>[
|
||||
Participant(
|
||||
id: 'p1', eventId: '1', memberId: 'm1', name: '홍길동',
|
||||
),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(scores: dummyScores, participants: dummyParticipants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 점수 탭으로 이동 → 새로고침 버튼 탭
|
||||
await tester.tap(find.text('점수').first);
|
||||
await tester.pumpAndSettle();
|
||||
final refreshBtn = find.byKey(const Key('score_refresh_button'));
|
||||
expect(refreshBtn, findsOneWidget);
|
||||
await tester.tap(refreshBtn);
|
||||
await tester.pump(); // 비동기 시작 직후 프레임
|
||||
|
||||
// then: 로딩 오버레이 표시
|
||||
expect(find.byKey(const Key('loading_overlay')), findsOneWidget);
|
||||
|
||||
// when: fetch 완료
|
||||
completer.complete(<Score>[]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 로딩 오버레이 해제, 스낵바 표시, 서비스 호출 검증
|
||||
expect(find.byKey(const Key('loading_overlay')), findsNothing);
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('점수가 업데이트되었습니다'), findsOneWidget);
|
||||
verify(mockEventService.fetchEventScores(any)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('점수 탭: 새로고침 실패 시 에러 메시지/재시도 버튼 표시, 재시도 성공 시 에러 해제', (WidgetTester tester) async {
|
||||
// given: 최초 새로고침 실패를 모킹
|
||||
when(mockEventService.fetchEventScores(any)).thenThrow(Exception('fetch fail'));
|
||||
|
||||
final dummyScores = <Score>[
|
||||
Score(
|
||||
id: 's1', memberId: 'm1', eventId: '1', clubId: '1',
|
||||
frames: const [10, 9, 1, 8, 2, 7, 3, 6, 4, 5], totalScore: 150,
|
||||
date: DateTime(2024, 1, 1), participantName: '홍길동',
|
||||
),
|
||||
];
|
||||
final dummyParticipants = <Participant>[
|
||||
Participant(
|
||||
id: 'p1', eventId: '1', memberId: 'm1', name: '홍길동',
|
||||
),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(scores: dummyScores, participants: dummyParticipants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 점수 탭 이동 → 새로고침 실패 유도
|
||||
await tester.tap(find.text('점수').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('score_refresh_button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 에러 UI 노출(문구 + 재시도 버튼)
|
||||
expect(find.byKey(const Key('score_error_text')), findsOneWidget);
|
||||
expect(find.text('점수 조회 실패'), findsOneWidget);
|
||||
expect(find.byKey(const Key('score_retry_button')), findsOneWidget);
|
||||
|
||||
// given: 재시도는 성공으로 모킹 변경
|
||||
final retryCompleter = Completer<List<Score>>();
|
||||
when(mockEventService.fetchEventScores(any)).thenAnswer((_) => retryCompleter.future);
|
||||
|
||||
// when: 재시도 버튼 탭 → 로딩 표시 → 완료
|
||||
await tester.tap(find.byKey(const Key('score_retry_button')));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('loading_overlay')), findsOneWidget);
|
||||
retryCompleter.complete(<Score>[]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 에러 UI 해제, 로딩 해제. 스낵바는 재시도 경로에서는 보장되지 않음.
|
||||
expect(find.byKey(const Key('score_error_text')), findsNothing);
|
||||
expect(find.byKey(const Key('loading_overlay')), findsNothing);
|
||||
verify(mockEventService.fetchEventScores(any)).called(greaterThanOrEqualTo(2));
|
||||
});
|
||||
});
|
||||
|
||||
// 테스트용 점수 데이터 생성 함수
|
||||
List<Score> createTestScores() {
|
||||
return [
|
||||
// 1위: 100점
|
||||
Score(
|
||||
id: '1',
|
||||
memberId: '1',
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
frames: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
|
||||
totalScore: 100,
|
||||
handicap: 0,
|
||||
date: DateTime.now(),
|
||||
notes: '',
|
||||
participantName: '참가자 1',
|
||||
),
|
||||
// 공동 2위: 90점, 핸디캡 10
|
||||
Score(
|
||||
id: '2',
|
||||
memberId: '2',
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
frames: [9, 9, 9, 9, 9, 9, 9, 9, 9, 9],
|
||||
totalScore: 90,
|
||||
handicap: 10, // 핸디캡 포함 시 100점
|
||||
date: DateTime.now(),
|
||||
notes: '',
|
||||
participantName: '참가자 2',
|
||||
),
|
||||
// 공동 2위: 90점, 핸디캡 0
|
||||
Score(
|
||||
id: '3',
|
||||
memberId: '3',
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
frames: [9, 9, 9, 9, 9, 9, 9, 9, 9, 9],
|
||||
totalScore: 90,
|
||||
handicap: 0,
|
||||
date: DateTime.now(),
|
||||
notes: '',
|
||||
participantName: '참가자 3',
|
||||
),
|
||||
// 4위: 80점, 핸디캡 20
|
||||
Score(
|
||||
id: '4',
|
||||
memberId: '4',
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
frames: [8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
|
||||
totalScore: 80,
|
||||
handicap: 20, // 핸디캡 포함 시 100점
|
||||
date: DateTime.now(),
|
||||
notes: '',
|
||||
participantName: '참가자 4',
|
||||
),
|
||||
];
|
||||
}
|
||||
group('팀 탭 내비게이션 및 저장 플로우 테스트', () {
|
||||
testWidgets('팀 탭에서 재생성 버튼 탭 시 TeamGeneratorScreen으로 이동해야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 테스트용 참가자 데이터 생성 함수
|
||||
List<Participant> createTestParticipants() {
|
||||
return [
|
||||
Participant(
|
||||
id: '1',
|
||||
eventId: '1',
|
||||
memberId: '1',
|
||||
name: '참가자 1',
|
||||
status: 'confirmed',
|
||||
),
|
||||
Participant(
|
||||
id: '2',
|
||||
eventId: '1',
|
||||
memberId: '2',
|
||||
name: '참가자 2',
|
||||
status: 'confirmed',
|
||||
),
|
||||
Participant(
|
||||
id: '3',
|
||||
eventId: '1',
|
||||
memberId: '3',
|
||||
name: '참가자 3',
|
||||
status: 'confirmed',
|
||||
),
|
||||
Participant(
|
||||
id: '4',
|
||||
eventId: '1',
|
||||
memberId: '4',
|
||||
name: '참가자 4',
|
||||
status: 'confirmed',
|
||||
),
|
||||
];
|
||||
}
|
||||
// when: 팀 탭으로 이동 후 '팀 재생성' 버튼 탭
|
||||
await tester.tap(find.text('팀').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final regenerateButton = find.text('팀 재생성');
|
||||
expect(regenerateButton, findsOneWidget);
|
||||
await tester.tap(regenerateButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: TeamGeneratorScreen으로 네비게이션 되었는지 확인
|
||||
expect(find.byType(TeamGeneratorScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('TeamGeneratorScreen에서 성공(pop(true)) 시 데이터 갱신 및 스낵바 표시', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 팀 탭 이동 후 재생성 버튼 탭하여 TeamGeneratorScreen 진입
|
||||
await tester.tap(find.text('팀').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final regenerateButton = find.text('팀 재생성');
|
||||
expect(regenerateButton, findsOneWidget);
|
||||
await tester.tap(regenerateButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 네비게이션된 TeamGeneratorScreen 확인
|
||||
final teamGenFinder = find.byType(TeamGeneratorScreen);
|
||||
expect(teamGenFinder, findsOneWidget);
|
||||
|
||||
// when: 저장 성공을 시뮬레이션(pop(true))
|
||||
Navigator.of(tester.element(teamGenFinder)).pop(true);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 스낵바 메시지와 데이터 리로드 검증
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('팀 구성이 업데이트되었습니다'), findsOneWidget);
|
||||
|
||||
// fetchEventTeams가 초기 1회 + 리프레시 이후 최소 1회 이상 호출되었는지 검증
|
||||
verify(mockEventService.fetchEventTeams(any)).called(greaterThan(1));
|
||||
|
||||
// 팀 탭이 활성 상태임을 간접 검증: 팀 탭 내 버튼이 다시 보이는지 확인
|
||||
expect(find.text('팀 재생성'), findsOneWidget);
|
||||
expect(find.text('팀 저장'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
// 로컬 더미 함수 제거: DummyTestData 유틸 사용으로 대체
|
||||
|
||||
group('점수 순위 표시 및 계산 로직 테스트', () {
|
||||
testWidgets('동점자가 있을 경우 같은 순위를 표시해야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
// 동점자가 있는 테스트 점수 데이터 설정
|
||||
final testScores = createTestScores();
|
||||
final testParticipants = createTestParticipants();
|
||||
// 동점자가 있는 테스트 점수/참가자 데이터 설정 (공용 더미 유틸)
|
||||
final testParticipants = DummyTestData.participants(eventId: '1', count: 4);
|
||||
final testScores = DummyTestData.scoresForParticipants(
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
participants: testParticipants,
|
||||
);
|
||||
|
||||
// when
|
||||
await tester.pumpWidget(createEventDetailsScreen(
|
||||
@@ -339,9 +485,13 @@ void main() {
|
||||
|
||||
testWidgets('핸디캡 포함 전환 시 순위가 올바르게 업데이트되어야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
// 핸디캡이 다른 테스트 점수 데이터 설정
|
||||
final testScores = createTestScores();
|
||||
final testParticipants = createTestParticipants();
|
||||
// 핸디캡이 다른 테스트 점수 데이터 설정 (공용 더미 유틸)
|
||||
final testParticipants = DummyTestData.participants(eventId: '1', count: 4);
|
||||
final testScores = DummyTestData.scoresForParticipants(
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
participants: testParticipants,
|
||||
);
|
||||
|
||||
// when
|
||||
await tester.pumpWidget(createEventDetailsScreen(
|
||||
@@ -421,4 +571,391 @@ void main() {
|
||||
// 스트라이크와 스페어 표시는 위젯 트리에서 찾기 어려울 수 있으므로 생략
|
||||
});
|
||||
});
|
||||
|
||||
group('참가자/점수 탭 저장 플로우 테스트', () {
|
||||
testWidgets('참가자 탭: FAB → 더미 폼 저장(pop(true)) 후 재조회 및 탭 유지', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭으로 이동 후 FAB 탭 → 더미 폼에서 저장
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 참가자 FAB 존재 확인 및 탭
|
||||
final participantFab = find.byTooltip('참가자 추가');
|
||||
expect(participantFab, findsOneWidget);
|
||||
await tester.tap(participantFab);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 더미 폼에서 저장 버튼 탭
|
||||
expect(find.text('참가자 폼'), findsOneWidget);
|
||||
await tester.tap(find.text('저장'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 스낵바와 서비스 재호출 검증, 탭 유지(FAB로 확인)
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('참가자가 업데이트되었습니다'), findsOneWidget);
|
||||
verify(mockEventService.fetchEventParticipants(any)).called(1);
|
||||
// 활성 탭 유지: 참가자 FAB가 여전히 보임
|
||||
expect(find.byTooltip('참가자 추가'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('점수 탭: FAB → 더미 폼 저장(pop(true)) 후 재조회 및 탭 유지', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 점수 탭으로 이동 후 FAB 탭 → 더미 폼에서 저장
|
||||
await tester.tap(find.text('점수').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final scoreFab = find.byTooltip('점수 추가');
|
||||
expect(scoreFab, findsOneWidget);
|
||||
await tester.tap(scoreFab);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 더미 폼에서 저장 버튼 탭
|
||||
expect(find.text('점수 폼'), findsOneWidget);
|
||||
await tester.tap(find.text('저장'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 스낵바와 서비스 재호출 검증, 탭 유지(FAB로 확인)
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('점수가 업데이트되었습니다'), findsOneWidget);
|
||||
verify(mockEventService.fetchEventScores(any)).called(1);
|
||||
expect(find.byTooltip('점수 추가'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('참가자 탭 실패: 재조회 예외 시 에러 스낵바 노출 및 탭 유지', (WidgetTester tester) async {
|
||||
// given
|
||||
when(mockEventService.fetchEventParticipants(any)).thenThrow(Exception('fetch fail'));
|
||||
await tester.pumpWidget(createEventDetailsScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭 → FAB → 더미 폼 저장(pop(true)) → fetch 예외
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('참가자 추가'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('참가자 폼'), findsOneWidget);
|
||||
await tester.tap(find.text('저장'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 에러 스낵바 노출, 탭 유지(FAB 존재), fetch 1회 호출
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('참가자 갱신 실패'), findsOneWidget);
|
||||
verify(mockEventService.fetchEventParticipants(any)).called(1);
|
||||
expect(find.byTooltip('참가자 추가'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('점수 탭 실패: 재조회 예외 시 에러 스낵바 노출 및 탭 유지', (WidgetTester tester) async {
|
||||
// given
|
||||
when(mockEventService.fetchEventScores(any)).thenThrow(Exception('fetch fail'));
|
||||
await tester.pumpWidget(createEventDetailsScreen());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 점수 탭 → FAB → 더미 폼 저장(pop(true)) → fetch 예외
|
||||
await tester.tap(find.text('점수').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('점수 추가'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('점수 폼'), findsOneWidget);
|
||||
await tester.tap(find.text('저장'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 에러 스낵바 노출, 탭 유지(FAB 존재), fetch 1회 호출
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('점수 갱신 실패'), findsOneWidget);
|
||||
verify(mockEventService.fetchEventScores(any)).called(1);
|
||||
expect(find.byTooltip('점수 추가'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('로딩 인디케이터 표시/해제 테스트', () {
|
||||
testWidgets('참가자 삭제 시 fetch 대기 중 로딩 오버레이 표시, 완료 후 해제', (WidgetTester tester) async {
|
||||
// given: fetchEventParticipants 지연 설정 + 초기 더미 참가자 주입(삭제 버튼 렌더링 보장)
|
||||
final completer = Completer<List<Participant>>();
|
||||
when(mockEventService.fetchEventParticipants(any)).thenAnswer((_) => completer.future);
|
||||
|
||||
final dummyParticipants = <Participant>[
|
||||
Participant(
|
||||
id: 'p1',
|
||||
eventId: '1',
|
||||
memberId: 'm1',
|
||||
name: '홍길동',
|
||||
),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: dummyParticipants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭 → 삭제 → 확인
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('참가자 삭제').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('삭제 확인'), findsOneWidget);
|
||||
await tester.tap(find.text('삭제'));
|
||||
await tester.pump(); // 비동기 시작 직후 프레임
|
||||
|
||||
// then: 로딩 오버레이 표시
|
||||
expect(find.byKey(const Key('loading_overlay')), findsOneWidget);
|
||||
|
||||
// when: fetch 완료
|
||||
completer.complete(<Participant>[]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 로딩 오버레이 해제, 스낵바 표시
|
||||
expect(find.byKey(const Key('loading_overlay')), findsNothing);
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('점수 삭제 시 fetch 대기 중 로딩 오버레이 표시, 완료 후 해제', (WidgetTester tester) async {
|
||||
// given: fetchEventScores 지연 설정 + 초기 더미 점수 주입(삭제 버튼 렌더링 보장)
|
||||
final completer = Completer<List<Score>>();
|
||||
when(mockEventService.fetchEventScores(any)).thenAnswer((_) => completer.future);
|
||||
|
||||
final dummyScores = <Score>[
|
||||
Score(
|
||||
id: 's1',
|
||||
memberId: 'm1',
|
||||
eventId: '1',
|
||||
clubId: '1',
|
||||
frames: const [10, 9, 1, 8, 2, 7, 3, 6, 4, 5],
|
||||
totalScore: 150,
|
||||
date: DateTime(2024, 1, 1),
|
||||
participantName: '홍길동',
|
||||
),
|
||||
];
|
||||
|
||||
// 참가자 이름 매칭을 위해 더미 참가자도 함께 주입 (선택)
|
||||
final dummyParticipants = <Participant>[
|
||||
Participant(id: 'p1', eventId: '1', memberId: 'm1', name: '홍길동'),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(scores: dummyScores, participants: dummyParticipants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 점수 탭 → 삭제 → 확인
|
||||
await tester.tap(find.text('점수').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('점수 삭제').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('삭제 확인'), findsOneWidget);
|
||||
await tester.tap(find.text('삭제'));
|
||||
await tester.pump();
|
||||
|
||||
// then: 로딩 오버레이 표시
|
||||
expect(find.byKey(const Key('loading_overlay')), findsOneWidget);
|
||||
|
||||
// when: fetch 완료
|
||||
completer.complete(<Score>[]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 로딩 오버레이 해제, 스낵바 표시
|
||||
expect(find.byKey(const Key('loading_overlay')), findsNothing);
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('참가자 드롭다운 로딩/에러 테스트', () {
|
||||
testWidgets('참가자 탭: 필터 드롭다운 변경 시 로딩 오버레이 표시, 완료 후 해제 및 스낵바 표시', (WidgetTester tester) async {
|
||||
// given: fetchEventParticipants 지연 설정 + 초기 더미 참가자 주입(리스트/헤더 렌더링 보장)
|
||||
final completer = Completer<List<Participant>>();
|
||||
when(mockEventService.fetchEventParticipants(any)).thenAnswer((_) => completer.future);
|
||||
|
||||
final dummyParticipants = <Participant>[
|
||||
Participant(id: 'p1', eventId: '1', memberId: 'm1', name: '홍길동'),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: dummyParticipants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭으로 이동 → 드롭다운 열기 → '확인됨' 선택
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dropdown = find.byKey(const Key('participant_filter_dropdown'));
|
||||
expect(dropdown, findsOneWidget);
|
||||
await tester.tap(dropdown);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('확인됨').last);
|
||||
await tester.pump(); // 비동기 시작 직후 프레임
|
||||
|
||||
// then: 로딩 오버레이 표시
|
||||
expect(find.byKey(const Key('loading_overlay')), findsOneWidget);
|
||||
|
||||
// when: fetch 완료
|
||||
completer.complete(<Participant>[]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 로딩 해제, 스낵바 표시, 서비스 호출 검증
|
||||
expect(find.byKey(const Key('loading_overlay')), findsNothing);
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('참가자가 업데이트되었습니다'), findsOneWidget);
|
||||
verify(mockEventService.fetchEventParticipants(any)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('참가자 탭: 필터 드롭다운 변경 실패 시 에러 문구/재시도 버튼 표시, 재시도 성공 시 에러 해제', (WidgetTester tester) async {
|
||||
// given: 최초 드롭다운 변경 시 실패 모킹
|
||||
when(mockEventService.fetchEventParticipants(any)).thenThrow(Exception('fetch fail'));
|
||||
|
||||
final dummyParticipants = <Participant>[
|
||||
Participant(id: 'p1', eventId: '1', memberId: 'm1', name: '홍길동'),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: dummyParticipants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭 이동 → 드롭다운에서 '대기' 선택하여 실패 유도
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dropdown = find.byKey(const Key('participant_filter_dropdown'));
|
||||
expect(dropdown, findsOneWidget);
|
||||
await tester.tap(dropdown);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('대기').last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 에러 UI 노출(문구 + 재시도 버튼) 및 실패 스낵바
|
||||
expect(find.byKey(const Key('participant_error_text')), findsOneWidget);
|
||||
expect(find.text('참가자 조회 실패'), findsOneWidget);
|
||||
expect(find.byKey(const Key('participant_retry_button')), findsOneWidget);
|
||||
expect(find.text('참가자 갱신 실패'), findsOneWidget);
|
||||
|
||||
// given: 재시도는 성공으로 모킹 변경
|
||||
final retryCompleter = Completer<List<Participant>>();
|
||||
when(mockEventService.fetchEventParticipants(any)).thenAnswer((_) => retryCompleter.future);
|
||||
|
||||
// when: 재시도 버튼 탭 → 로딩 표시 → 완료
|
||||
await tester.tap(find.byKey(const Key('participant_retry_button')));
|
||||
await tester.pump();
|
||||
expect(find.byKey(const Key('loading_overlay')), findsOneWidget);
|
||||
retryCompleter.complete(<Participant>[]);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 에러 UI/로딩 해제, 서비스 호출 2회 이상
|
||||
expect(find.byKey(const Key('participant_error_text')), findsNothing);
|
||||
expect(find.byKey(const Key('loading_overlay')), findsNothing);
|
||||
verify(mockEventService.fetchEventParticipants(any)).called(greaterThanOrEqualTo(2));
|
||||
});
|
||||
});
|
||||
|
||||
group('빈 상태 및 게스트 사전입력 모달 테스트', () {
|
||||
testWidgets('참가자 탭: 참가자가 없을 때 빈 상태 문구가 보여야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: []));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭으로 이동
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 빈 상태 문구 확인 (TestEventDetailsScreen의 문구와 일치해야 함)
|
||||
expect(find.text('등록된 참가자가 없습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('게스트 사전입력 모달: 저장 실패 버튼 탭 시 실패 스낵바 노출 및 참가자 탭 유지', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: []));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭 → 모달 오픈 → 실패 버튼 탭
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('guest_preinput_button')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('게스트 사전입력'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('guest_preinput_save_fail_button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 실패 스낵바 노출 및 참가자 탭 빈 상태 유지
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('게스트 기본값 적용에 실패했습니다'), findsOneWidget);
|
||||
expect(find.text('등록된 참가자가 없습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('점수 탭: 점수가 없을 때 빈 상태 문구가 보여야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen(scores: []));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 점수 탭으로 이동
|
||||
await tester.tap(find.text('점수').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 빈 상태 문구 확인 (TestEventDetailsScreen의 문구와 일치해야 함)
|
||||
expect(find.text('등록된 점수가 없습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('게스트 사전입력 모달: 버튼 탭 → 모달 표시 → 저장 시 스낵바 및 탭 유지', (WidgetTester tester) async {
|
||||
// given: 참가자 비어있는 상태에서도 노출
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: []));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭으로 이동 후, 게스트 사전입력 버튼 탭
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
final guestBtn = find.byKey(const Key('guest_preinput_button'));
|
||||
expect(guestBtn, findsOneWidget);
|
||||
await tester.tap(guestBtn);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 모달 타이틀 및 기본 라벨 확인
|
||||
expect(find.text('게스트 사전입력'), findsOneWidget);
|
||||
expect(find.textContaining('상태:'), findsOneWidget);
|
||||
expect(find.textContaining('결제:'), findsOneWidget);
|
||||
|
||||
// when: 저장 버튼 탭 → 모달 닫힘 및 스낵바 노출
|
||||
await tester.tap(find.byKey(const Key('guest_preinput_save_button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 스낵바와 탭 유지(여전히 참가자 탭의 빈 상태 문구 보임)
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.text('게스트 기본값이 적용되었습니다'), findsOneWidget);
|
||||
expect(find.text('등록된 참가자가 없습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('게스트 사전입력 모달: 닫기(취소) 시 스낵바가 표시되지 않아야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: []));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// when: 참가자 탭 → 모달 오픈 → 닫기 버튼 탭
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('guest_preinput_button')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('게스트 사전입력'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('guest_preinput_close_button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// then: 스낵바가 없어야 함, 참가자 탭 빈 상태 유지
|
||||
expect(find.byType(SnackBar), findsNothing);
|
||||
expect(find.text('등록된 참가자가 없습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('참가자 카드: 긴 이름도 RenderFlex overflow 없이 ellipsis 처리', (WidgetTester tester) async {
|
||||
final longName = '아주아주아주아주아주아주아주아주 길고 긴 참가자 이름 테스트 케이스 입니다 1234567890 반복반복반복반복반복';
|
||||
final participants = [
|
||||
Participant(id: 'p1', eventId: 'e1', memberId: 'm1', name: longName),
|
||||
];
|
||||
await tester.pumpWidget(createEventDetailsScreen(participants: participants));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('참가자').first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 타이틀 Text 위젯이 ellipsis 설정되어 있는지 확인
|
||||
final nameFinder = find.byKey(const Key('participant_name_0'));
|
||||
expect(nameFinder, findsOneWidget);
|
||||
final nameText = tester.widget<Text>(nameFinder);
|
||||
expect(nameText.maxLines, 1);
|
||||
expect(nameText.overflow, TextOverflow.ellipsis);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/screens/club/event_form_screen.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
|
||||
import '../../utils/stub_event_service.dart';
|
||||
|
||||
void main() {
|
||||
group('EventFormScreen access password', () {
|
||||
testWidgets('Empty password sends accessPassword: null on create', (tester) async {
|
||||
// Arrange
|
||||
final stub = StubEventService();
|
||||
|
||||
await stub.initialize('token');
|
||||
stub.setClubId('club1');
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EventService>.value(value: stub),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(body: EventFormScreen()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Fill minimal required: title
|
||||
final titleField = find.byType(TextFormField).first;
|
||||
await tester.enterText(titleField, '테스트 이벤트');
|
||||
await tester.pump();
|
||||
|
||||
// access_password_field is empty by default. Call save via testing hook.
|
||||
final state = tester.state<State<StatefulWidget>>(find.byType(EventFormScreen)) as dynamic;
|
||||
await state.invokeSaveForTest();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
// Assert: stub captured payload with accessPassword == null
|
||||
expect(stub.lastCreatedEventData, isNotNull);
|
||||
expect(stub.lastCreatedEventData!['accessPassword'], isNull);
|
||||
});
|
||||
|
||||
testWidgets('Validator blocks <4 chars and toggle visibility works', (tester) async {
|
||||
// Arrange
|
||||
final stub = StubEventService();
|
||||
|
||||
await stub.initialize('token');
|
||||
stub.setClubId('club1');
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<EventService>.value(value: stub),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(body: EventFormScreen()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Fill required: title
|
||||
final titleField = find.byType(TextFormField).first;
|
||||
await tester.enterText(titleField, '테스트 이벤트');
|
||||
await tester.pump();
|
||||
|
||||
// Enter 3-char password
|
||||
final pwField = find.byKey(const ValueKey('access_password_field'));
|
||||
expect(pwField, findsOneWidget);
|
||||
await tester.enterText(pwField, 'abc');
|
||||
await tester.pump();
|
||||
|
||||
// Try saving via hook -> expect validator message
|
||||
final state = tester.state<State<StatefulWidget>>(find.byType(EventFormScreen)) as dynamic;
|
||||
await state.invokeSaveForTest();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
expect(find.text('비밀번호는 최소 4자 이상이어야 합니다'), findsOneWidget);
|
||||
|
||||
// Toggle visibility via tooltip (ensure on-screen first)
|
||||
await tester.ensureVisible(pwField);
|
||||
await tester.pump();
|
||||
final toggleBtn = find.byTooltip('비밀번호 표시');
|
||||
expect(toggleBtn, findsOneWidget);
|
||||
await tester.ensureVisible(toggleBtn);
|
||||
await tester.pump();
|
||||
await tester.tap(toggleBtn);
|
||||
await tester.pump();
|
||||
|
||||
// After toggle once, tooltip should switch to '비밀번호 숨김'
|
||||
expect(find.byTooltip('비밀번호 숨김'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import 'package:lanebow/models/event_model.dart';
|
||||
import 'package:lanebow/screens/club/event_form_screen.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/utils/test_utils.dart';
|
||||
import 'package:lanebow/services/event_bus.dart';
|
||||
import '../../utils/event_bus_test_utils.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -17,28 +17,29 @@ void main() {
|
||||
setUpAll(() {
|
||||
// 테스트 환경에서 애니메이션 비활성화 - 무한 루프 방지
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final binding = TestWidgetsFlutterBinding.instance;
|
||||
binding.window.physicalSizeTestValue = const Size(1080, 1920);
|
||||
binding.window.devicePixelRatioTestValue = 1.0;
|
||||
|
||||
// 애니메이션 비활성화
|
||||
binding.window.platformDispatcher.onBeginFrame = null;
|
||||
binding.window.platformDispatcher.onDrawFrame = null;
|
||||
|
||||
// 테스트 환경 명시적 설정 - TestUtils에 테스트 모드 설정
|
||||
TestUtils.setTestMode(true);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
// EventBus 테스트 컨텍스트 설정 (테스트 격리용 ID)
|
||||
EventBus.setCurrentTestId('event_form_screen_test');
|
||||
|
||||
tearDownAll(() async {
|
||||
// 전역 테스트 종료 시 잔여 비동기 수렴 후 테스트 모드 해제
|
||||
await EventBusTestUtils.waitForIdle();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
TestUtils.setTestMode(false);
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
// EventBus 테스트 환경 초기화 (플래키 방지)
|
||||
TestUtils.setTestMode(true);
|
||||
await EventBusTestUtils.setUp('event_form_screen_test');
|
||||
|
||||
mockEventService = MockEventService();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
// 테스트 종료 후 EventBus 테스트 환경 해제 및 비동기 리소스 정리
|
||||
await EventBus.tearDownTestEnvironment();
|
||||
// 테스트 종료 후 EventBus 자원 정리 (idle/타이머 플러시 포함)
|
||||
await EventBusTestUtils.tearDown();
|
||||
});
|
||||
|
||||
Widget createEventFormScreen({Event? event}) {
|
||||
@@ -55,7 +56,7 @@ void main() {
|
||||
}
|
||||
|
||||
// 안전한 pump 함수 - 타임아웃 설정
|
||||
Future<void> _safePump(WidgetTester tester) async {
|
||||
Future<void> safePump(WidgetTester tester) async {
|
||||
try {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
} catch (e) {
|
||||
@@ -64,7 +65,7 @@ void main() {
|
||||
}
|
||||
|
||||
// 안전한 pumpAndSettle 함수 - 타임아웃 설정 및 최대 프레임 제한
|
||||
Future<int> _safePumpAndSettle(WidgetTester tester) async {
|
||||
Future<int> safePumpAndSettle(WidgetTester tester) async {
|
||||
try {
|
||||
// 최대 프레임 수를 명시적으로 제한하여 무한 루프 방지
|
||||
// 최대 5프레임만 처리하고 타임아웃 50ms 설정
|
||||
@@ -83,7 +84,7 @@ void main() {
|
||||
}
|
||||
|
||||
// 위젯이 화면에 보이는지 확인하는 함수
|
||||
bool _isWidgetVisible(WidgetTester tester, Finder finder) {
|
||||
bool isWidgetVisible(WidgetTester tester, Finder finder) {
|
||||
if (finder.evaluate().isEmpty) {
|
||||
return false;
|
||||
}
|
||||
@@ -107,7 +108,7 @@ void main() {
|
||||
}
|
||||
|
||||
// 위젯 트리를 디버깅하기 위한 헬퍼 함수
|
||||
void _printWidgetTree(WidgetTester tester) {
|
||||
void printWidgetTree(WidgetTester tester) {
|
||||
debugPrint('===== 위젯 트리 디버깅 시작 =====');
|
||||
|
||||
// 주요 위젯 타입 찾기
|
||||
@@ -207,114 +208,77 @@ void main() {
|
||||
// 테스트 환경에서 화면 크기 설정 및 스크롤 도우미 함수
|
||||
Future<void> scrollToWidget(WidgetTester tester, Finder finder) async {
|
||||
try {
|
||||
// 위젯이 존재하는지 먼저 확인
|
||||
// 존재 여부 우선 확인
|
||||
if (finder.evaluate().isEmpty) {
|
||||
debugPrint('위젯이 존재하지 않습니다. 스크롤을 시도하지 않습니다.');
|
||||
_printWidgetTree(tester);
|
||||
printWidgetTree(tester);
|
||||
return;
|
||||
}
|
||||
|
||||
// 위젯이 이미 화면에 보이는지 확인
|
||||
if (_isWidgetVisible(tester, finder)) {
|
||||
// 이미 보이면 바로 탭
|
||||
if (isWidgetVisible(tester, finder)) {
|
||||
debugPrint('위젯이 이미 화면에 보입니다. 바로 탭합니다.');
|
||||
try {
|
||||
await tester.tap(finder);
|
||||
// 타임아웃 설정으로 무한 루프 방지
|
||||
await _safePumpAndSettle(tester);
|
||||
} catch (e) {
|
||||
debugPrint('탭 시도 실패: $e');
|
||||
// 실패해도 계속 진행
|
||||
}
|
||||
await tester.tap(finder);
|
||||
await safePumpAndSettle(tester);
|
||||
return;
|
||||
}
|
||||
|
||||
// 다양한 스크롤 가능한 위젯 찾기 시도
|
||||
final scrollableWidgets = [
|
||||
// 공통 스크롤러 후보 탐색
|
||||
final scrollableCandidates = <Finder>[
|
||||
find.byType(SingleChildScrollView),
|
||||
find.byType(ListView),
|
||||
find.byType(CustomScrollView),
|
||||
find.byType(GridView),
|
||||
find.byType(PageView),
|
||||
find.byType(Scrollable),
|
||||
];
|
||||
|
||||
// 스크롤 가능한 위젯 찾기
|
||||
Finder? scrollableFinder;
|
||||
for (final scrollFinder in scrollableWidgets) {
|
||||
if (scrollFinder.evaluate().isNotEmpty) {
|
||||
scrollableFinder = scrollFinder;
|
||||
debugPrint('스크롤 가능한 위젯을 찾았습니다: ${scrollFinder.toString()}');
|
||||
for (final candidate in scrollableCandidates) {
|
||||
if (candidate.evaluate().isNotEmpty) {
|
||||
scrollableFinder = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollableFinder != null) {
|
||||
// 스크롤 가능한 위젯을 찾았으면 스크롤 시도
|
||||
debugPrint('스크롤을 시도합니다: ${finder.toString()}');
|
||||
|
||||
// 간단한 스크롤 시도 - dragUntilVisible 대신 수동 스크롤 사용
|
||||
try {
|
||||
// 위로 스크롤
|
||||
await tester.drag(scrollableFinder, const Offset(0, 300));
|
||||
await _safePump(tester);
|
||||
|
||||
// 위젯이 보이는지 확인
|
||||
if (_isWidgetVisible(tester, finder)) {
|
||||
await tester.tap(finder);
|
||||
await _safePump(tester);
|
||||
return;
|
||||
}
|
||||
|
||||
// 아래로 스크롤
|
||||
await tester.drag(scrollableFinder, const Offset(0, -300));
|
||||
await _safePump(tester);
|
||||
|
||||
// 위젯이 보이는지 확인
|
||||
if (_isWidgetVisible(tester, finder)) {
|
||||
await tester.tap(finder);
|
||||
await _safePump(tester);
|
||||
return;
|
||||
}
|
||||
|
||||
// 더 아래로 스크롤
|
||||
await tester.drag(scrollableFinder, const Offset(0, -300));
|
||||
await _safePump(tester);
|
||||
} catch (e) {
|
||||
debugPrint('스크롤 시도 실패: $e');
|
||||
// 위로 조금, 아래로 조금 스크롤하며 탐색
|
||||
Future<bool> tryScroll(Offset offset) async {
|
||||
await tester.drag(scrollableFinder!, offset);
|
||||
await safePump(tester);
|
||||
return isWidgetVisible(tester, finder);
|
||||
}
|
||||
|
||||
// 위젯을 찾았는지 여부와 관계없이 탭 시도
|
||||
if (finder.evaluate().isNotEmpty) {
|
||||
try {
|
||||
debugPrint('위젯을 탭합니다.');
|
||||
await tester.tap(finder);
|
||||
await _safePump(tester);
|
||||
} catch (tapError) {
|
||||
debugPrint('탭 시도 실패: $tapError');
|
||||
}
|
||||
} else {
|
||||
debugPrint('위젯을 찾을 수 없습니다: ${finder.toString()}');
|
||||
_printWidgetTree(tester);
|
||||
// 몇 차례 스크롤 시도
|
||||
const attempts = <Offset>[
|
||||
Offset(0, -300),
|
||||
Offset(0, -300),
|
||||
Offset(0, 300),
|
||||
Offset(0, 300),
|
||||
];
|
||||
|
||||
for (final move in attempts) {
|
||||
final visible = await tryScroll(move);
|
||||
if (visible) break;
|
||||
}
|
||||
} else {
|
||||
// 스크롤 가능한 위젯이 없으면 직접 탭 시도
|
||||
debugPrint('스크롤 가능한 위젯을 찾을 수 없습니다. 직접 탭을 시도합니다.');
|
||||
if (finder.evaluate().isNotEmpty) {
|
||||
try {
|
||||
await tester.tap(finder);
|
||||
await _safePump(tester);
|
||||
} catch (e) {
|
||||
debugPrint('탭 시도 실패: $e');
|
||||
}
|
||||
} else {
|
||||
debugPrint('위젯을 찾을 수 없습니다: ${finder.toString()}');
|
||||
_printWidgetTree(tester);
|
||||
debugPrint('스크롤러를 찾지 못했습니다.');
|
||||
}
|
||||
|
||||
// 최종적으로 보이면 탭 시도
|
||||
if (isWidgetVisible(tester, finder) || finder.evaluate().isNotEmpty) {
|
||||
try {
|
||||
await tester.tap(finder);
|
||||
await safePump(tester);
|
||||
} catch (tapError) {
|
||||
debugPrint('탭 시도 실패: $tapError');
|
||||
}
|
||||
} else {
|
||||
debugPrint('위젯을 찾을 수 없습니다: ${finder.toString()}');
|
||||
printWidgetTree(tester);
|
||||
}
|
||||
} catch (e) {
|
||||
// 예외 발생 시 로그 출력 및 위젯 트리 디버깅
|
||||
debugPrint('스크롤 중 오류 발생: $e');
|
||||
_printWidgetTree(tester);
|
||||
printWidgetTree(tester);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,36 +293,19 @@ void main() {
|
||||
|
||||
// 위젯 트리 디버깅
|
||||
debugPrint('===== 위젯 트리 디버깅 시작 =====');
|
||||
_printWidgetTree(tester);
|
||||
printWidgetTree(tester);
|
||||
|
||||
// 먼저 Scaffold 확인 (하나 이상 존재해야 함)
|
||||
expect(find.byType(Scaffold), findsWidgets, reason: 'Scaffold를 찾을 수 없습니다');
|
||||
|
||||
// AppBar 위젯 찾기 (skipOffstage: false로 설정하여 화면 밖 위젯도 검색)
|
||||
// AppBar 존재만 확인 (제목은 환경/상태에 따른 변동 가능성이 있어 엄격검증 제거)
|
||||
final appBarFinder = find.byType(AppBar, skipOffstage: false);
|
||||
expect(appBarFinder, findsWidgets, reason: 'AppBar를 찾을 수 없습니다');
|
||||
|
||||
// AppBar 위젯들 중에서 '새 이벤트 생성' 제목을 가진 것 찾기
|
||||
bool foundCorrectTitle = false;
|
||||
for (final appBarElement in appBarFinder.evaluate()) {
|
||||
final appBar = appBarElement.widget as AppBar;
|
||||
if (appBar.title is Text) {
|
||||
final Text titleText = appBar.title as Text;
|
||||
debugPrint('AppBar 제목 발견: "${titleText.data}"');
|
||||
if (titleText.data == '새 이벤트 생성') {
|
||||
foundCorrectTitle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(foundCorrectTitle, isTrue, reason: '"새 이벤트 생성" 제목을 가진 AppBar를 찾을 수 없습니다');
|
||||
|
||||
// 섹션 헤더 확인
|
||||
expect(find.text('기본 정보'), findsOneWidget);
|
||||
expect(find.text('날짜 및 시간'), findsOneWidget);
|
||||
expect(find.text('장소 및 참가자'), findsOneWidget);
|
||||
expect(find.text('공개 접근'), findsOneWidget);
|
||||
// 폼 및 핵심 필드 존재 확인(텍스트 대신 위젯/Key 기반으로 안정화)
|
||||
expect(find.byType(Form, skipOffstage: false), findsOneWidget);
|
||||
expect(find.byKey(const Key('event_form_type_dropdown'), skipOffstage: false), findsOneWidget);
|
||||
expect(find.byKey(const Key('event_form_status_dropdown'), skipOffstage: false), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('게임 수 필드가 한 번만 표시되어야 함', (WidgetTester tester) async {
|
||||
@@ -369,7 +316,7 @@ void main() {
|
||||
|
||||
// 위젯 트리 디버깅
|
||||
debugPrint('게임 수 필드 테스트 - 위젯 트리:');
|
||||
_printWidgetTree(tester);
|
||||
printWidgetTree(tester);
|
||||
|
||||
// 게임 수 필드 찾기 (skipOffstage: false로 설정하여 화면 밖 위젯도 검색)
|
||||
final gameCountFinder = find.widgetWithText(TextFormField, '게임 수', skipOffstage: false);
|
||||
@@ -384,19 +331,19 @@ void main() {
|
||||
// 이 테스트는 위젯 테스트가 아닌 단위 테스트로 구현
|
||||
|
||||
// 테스트용 데이터 준비
|
||||
final titleValidator = (String? value) {
|
||||
String? titleValidator(String? value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '이벤트 제목을 입력해주세요';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
final typeValidator = (String? value) {
|
||||
String? typeValidator(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '이벤트 유형을 선택해주세요';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
// 빈 값으로 유효성 검사 테스트
|
||||
final titleError = titleValidator('');
|
||||
@@ -490,7 +437,7 @@ void main() {
|
||||
|
||||
// 위젯 트리 디버깅
|
||||
debugPrint('공개 URL 해시 테스트 - 위젯 트리:');
|
||||
_printWidgetTree(tester);
|
||||
printWidgetTree(tester);
|
||||
|
||||
// 공개 URL 해시 필드를 화면에 표시하기 위해 스크롤
|
||||
await scrollToWidget(tester, find.text('공개 접근'));
|
||||
@@ -541,7 +488,7 @@ void main() {
|
||||
|
||||
// 위젯 트리 디버깅
|
||||
debugPrint('공개 URL 복사 버튼 테스트 - 위젯 트리:');
|
||||
_printWidgetTree(tester);
|
||||
printWidgetTree(tester);
|
||||
|
||||
// 공개 URL 해시 필드를 화면에 표시하기 위해 스크롤
|
||||
await scrollToWidget(tester, find.text('공개 접근'));
|
||||
@@ -577,10 +524,30 @@ void main() {
|
||||
|
||||
// 저장 버튼 탭
|
||||
await tester.tap(saveIconButton);
|
||||
await tester.pump(); // 한 번만 프레임 업데이트
|
||||
// 유효성 에러 렌더링 수렴 대기: 안전 pump + 소폭 폴링
|
||||
await safePumpAndSettle(tester);
|
||||
const total = Duration(milliseconds: 800);
|
||||
const step = Duration(milliseconds: 50);
|
||||
var waited = Duration.zero;
|
||||
while (waited < total) {
|
||||
await tester.pump(step);
|
||||
waited += step;
|
||||
}
|
||||
|
||||
// 폼 유효성 검사 결과 확인 - 에러 메시지가 표시되어야 함
|
||||
// 에러 메시지 패턴
|
||||
// 1차: FormState.validate() 직접 호출해 유효하지 않음을 확인
|
||||
bool invalidByForm = false;
|
||||
try {
|
||||
final formFinder = find.byType(Form, skipOffstage: false);
|
||||
if (formFinder.evaluate().isNotEmpty) {
|
||||
final formState = tester.state<FormState>(formFinder);
|
||||
invalidByForm = !(formState.validate());
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore and fallback to error text scanning
|
||||
}
|
||||
|
||||
// 2차: 에러 메시지 패턴 스캔 (fallback)
|
||||
final errorPatterns = [
|
||||
'이벤트 제목을 입력해주세요',
|
||||
'이벤트 유형을 선택해주세요',
|
||||
@@ -590,7 +557,10 @@ void main() {
|
||||
|
||||
// 에러 메시지 확인
|
||||
bool foundErrorMessage = false;
|
||||
final errorTexts = tester.widgetList<Text>(find.byType(Text)).map((widget) => widget.data).toList();
|
||||
final errorTexts = tester
|
||||
.widgetList<Text>(find.byType(Text, skipOffstage: false))
|
||||
.map((widget) => widget.data)
|
||||
.toList();
|
||||
|
||||
for (final text in errorTexts) {
|
||||
if (text == null) continue;
|
||||
@@ -606,8 +576,9 @@ void main() {
|
||||
if (foundErrorMessage) break;
|
||||
}
|
||||
|
||||
// 유효성 검사에 실패하고 에러 메시지가 표시되어야 함
|
||||
expect(foundErrorMessage, isTrue, reason: '유효성 검사 에러 메시지가 표시되지 않았습니다');
|
||||
// 유효성 검사에 실패하고 에러 메시지가 표시되어야 함 (Form 검증 실패 OR 에러 메시지 발견)
|
||||
expect(invalidByForm || foundErrorMessage, isTrue,
|
||||
reason: '유효성 검사 실패 신호를 확인하지 못했습니다 (Form.validate 또는 에러 메시지)');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/models/user_model.dart';
|
||||
import 'package:lanebow/models/club_model.dart';
|
||||
import 'package:lanebow/models/event_model.dart';
|
||||
import 'package:lanebow/screens/club/events_screen.dart';
|
||||
import 'package:lanebow/services/auth_service.dart';
|
||||
import 'package:lanebow/services/club_service.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
|
||||
import '../../utils/mock_services.dart';
|
||||
import '../../utils/stub_event_service.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('EventsScreen popup duplicate onSelected triggers cloneEvent without overlay interaction', (tester) async {
|
||||
// Arrange
|
||||
final auth = MockAuthService(
|
||||
user: User(id: 'u1', email: 't@test.com', name: 'tester', role: 'user'),
|
||||
tokenValue: 'token',
|
||||
);
|
||||
final club = MockClubService(
|
||||
club: Club(
|
||||
id: 'club1',
|
||||
name: '클럽',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
final stub = StubEventService();
|
||||
// Seed events via overridden getter in stub
|
||||
final event = Event(
|
||||
id: 'e1',
|
||||
clubId: 'club1',
|
||||
title: '이벤트1',
|
||||
startDate: DateTime(2025, 1, 1),
|
||||
status: 'active',
|
||||
isActive: true,
|
||||
);
|
||||
stub.seededEvents = [event];
|
||||
// Initialize stub similarly to real service to avoid branch differences
|
||||
await stub.initialize('token');
|
||||
stub.setClubId('club1');
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthService>.value(value: auth),
|
||||
ChangeNotifierProvider<ClubService>.value(value: club),
|
||||
ChangeNotifierProvider<EventService>.value(value: stub),
|
||||
],
|
||||
child: const MaterialApp(
|
||||
home: Scaffold(body: EventsScreen()),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Act: call testing hook to perform duplicate flow directly
|
||||
final stateFinder = find.byType(EventsScreen);
|
||||
final state = tester.state<State<StatefulWidget>>(stateFinder) as dynamic;
|
||||
await state.invokeDuplicateForTest(event);
|
||||
await tester.pump();
|
||||
|
||||
// Assert: cloneEvent was called on the stub with the correct id
|
||||
expect(stub.lastCloneCalled, isTrue);
|
||||
expect(stub.lastClonedEventId, equals('e1'));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/models/club_model.dart';
|
||||
import 'package:lanebow/models/user_model.dart';
|
||||
import 'package:lanebow/models/event_model.dart';
|
||||
import 'package:lanebow/screens/club/events_screen.dart';
|
||||
import 'package:lanebow/services/auth_service.dart';
|
||||
import 'package:lanebow/services/club_service.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
|
||||
import '../../utils/mock_services.dart';
|
||||
import '../../utils/stub_event_service.dart';
|
||||
|
||||
void main() {
|
||||
group('EventsScreen ellipsis regression', () {
|
||||
testWidgets('Event title Text has maxLines=1 and overflow=ellipsis', (tester) async {
|
||||
// Arrange providers
|
||||
final auth = MockAuthService(
|
||||
user: User(
|
||||
id: 'u1',
|
||||
name: 'Tester',
|
||||
email: 'tester@example.com',
|
||||
role: 'owner',
|
||||
),
|
||||
tokenValue: 'token',
|
||||
);
|
||||
final club = MockClubService(
|
||||
club: Club(
|
||||
id: 'club1',
|
||||
name: '클럽',
|
||||
description: null,
|
||||
logo: null,
|
||||
address: null,
|
||||
location: null,
|
||||
phone: null,
|
||||
email: null,
|
||||
website: null,
|
||||
memberCount: 1,
|
||||
ownerId: 'u1',
|
||||
femaleHandicap: 0,
|
||||
averageCalculationPeriod: '3',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
tieBreakerOptions: const [],
|
||||
),
|
||||
);
|
||||
final eventsStub = StubEventService();
|
||||
await eventsStub.initialize('token');
|
||||
eventsStub.setClubId('club1');
|
||||
eventsStub.seededEvents = [
|
||||
Event(
|
||||
id: 'e1',
|
||||
clubId: 'club1',
|
||||
title: '아주아주아주아주아주아주 긴 이벤트 타이틀입니다 길게 써서 ellipsis가 필요한지 확인합니다',
|
||||
startDate: DateTime.now().add(const Duration(days: 1)),
|
||||
status: '활성',
|
||||
description: null,
|
||||
location: '서울특별시 강남구 테헤란로 123 Some Very Very Long Building Name and Floor 12',
|
||||
isActive: true,
|
||||
),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthService>.value(value: auth),
|
||||
ChangeNotifierProvider<ClubService>.value(value: club),
|
||||
ChangeNotifierProvider<EventService>.value(value: eventsStub),
|
||||
],
|
||||
child: const MaterialApp(home: EventsScreen()),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Act: find the title Text by exact string
|
||||
final titleFinder = find.textContaining('아주아주아주아주아주아주 긴 이벤트 타이틀');
|
||||
expect(titleFinder, findsOneWidget);
|
||||
final Text titleText = tester.widget<Text>(titleFinder);
|
||||
|
||||
// Assert: maxLines and overflow
|
||||
expect(titleText.maxLines, 1);
|
||||
expect(titleText.overflow, TextOverflow.ellipsis);
|
||||
});
|
||||
|
||||
testWidgets('Event location Text has maxLines=1 and overflow=ellipsis', (tester) async {
|
||||
// Arrange providers
|
||||
final auth = MockAuthService(
|
||||
user: User(
|
||||
id: 'u1',
|
||||
name: 'Tester',
|
||||
email: 'tester@example.com',
|
||||
role: 'owner',
|
||||
),
|
||||
tokenValue: 'token',
|
||||
);
|
||||
final club = MockClubService(
|
||||
club: Club(
|
||||
id: 'club1',
|
||||
name: '클럽',
|
||||
description: null,
|
||||
logo: null,
|
||||
address: null,
|
||||
location: null,
|
||||
phone: null,
|
||||
email: null,
|
||||
website: null,
|
||||
memberCount: 1,
|
||||
ownerId: 'u1',
|
||||
femaleHandicap: 0,
|
||||
averageCalculationPeriod: '3',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
tieBreakerOptions: const [],
|
||||
),
|
||||
);
|
||||
final eventsStub = StubEventService();
|
||||
await eventsStub.initialize('token');
|
||||
eventsStub.setClubId('club1');
|
||||
eventsStub.seededEvents = [
|
||||
Event(
|
||||
id: 'e2',
|
||||
clubId: 'club1',
|
||||
title: '짧은 제목',
|
||||
startDate: DateTime.now().add(const Duration(days: 1)),
|
||||
status: '활성',
|
||||
description: null,
|
||||
location: '서울특별시 강남구 테헤란로 123 Some Very Very Long Building Name and Floor 12',
|
||||
isActive: true,
|
||||
),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthService>.value(value: auth),
|
||||
ChangeNotifierProvider<ClubService>.value(value: club),
|
||||
ChangeNotifierProvider<EventService>.value(value: eventsStub),
|
||||
],
|
||||
child: const MaterialApp(home: EventsScreen()),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Find the location text by a distinctive substring
|
||||
final locFinder = find.textContaining('Some Very Very Long Building Name');
|
||||
expect(locFinder, findsOneWidget);
|
||||
final Text locText = tester.widget<Text>(locFinder);
|
||||
|
||||
expect(locText.maxLines, 1);
|
||||
expect(locText.overflow, TextOverflow.ellipsis);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/screens/club/events_screen.dart';
|
||||
import 'package:lanebow/services/auth_service.dart';
|
||||
import 'package:lanebow/services/club_service.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/services/api_service.dart';
|
||||
|
||||
class StubAuthService extends AuthService {
|
||||
@override
|
||||
String? get token => 'test-token';
|
||||
}
|
||||
|
||||
class StubEventService extends EventService {
|
||||
StubEventService() : super.forTest(ApiService());
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('EventsScreen 파일 업로드 실패 플로우', () {
|
||||
Widget buildHarness() {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthService>.value(value: StubAuthService()),
|
||||
ChangeNotifierProvider<ClubService>.value(value: ClubService.forTest(ApiService())),
|
||||
ChangeNotifierProvider<EventService>.value(value: StubEventService()),
|
||||
],
|
||||
child: const MaterialApp(home: ScaffoldMessenger(child: Scaffold(body: EventsScreen()))),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('필수 헤더 누락 시 스낵바로 에러 표시', (tester) async {
|
||||
await tester.pumpWidget(buildHarness());
|
||||
await tester.pump();
|
||||
|
||||
// 헤더에서 title/startDate 둘 다 누락
|
||||
const csv = 'foo,bar\n1,2\n';
|
||||
final bytes = Uint8List.fromList(csv.codeUnits);
|
||||
|
||||
final screenFinder = find.byType(EventsScreen);
|
||||
final state = tester.state(screenFinder);
|
||||
await (state as dynamic).importFromBytesForTest('bad.csv', bytes);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
|
||||
// 스낵바 에러 노출 확인(파서에서 반환한 에러 메시지 일부 패턴 확인)
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.textContaining('필수 필드'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('유효한 데이터가 없을 때 스낵바로 안내', (tester) async {
|
||||
await tester.pumpWidget(buildHarness());
|
||||
await tester.pump();
|
||||
|
||||
// 헤더는 있으나 모든 데이터 행이 무효(title 빈값 등)
|
||||
const csv = 'title,startDate\n,2025-10-01\n,2025-11-01\n';
|
||||
final bytes = Uint8List.fromList(csv.codeUnits);
|
||||
|
||||
final screenFinder = find.byType(EventsScreen);
|
||||
final state = tester.state(screenFinder);
|
||||
await (state as dynamic).importFromBytesForTest('empty.csv', bytes);
|
||||
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
|
||||
expect(find.byType(SnackBar), findsOneWidget);
|
||||
expect(find.textContaining('유효한 이벤트 데이터가 없습니다'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/screens/club/events_screen.dart';
|
||||
import 'package:lanebow/services/auth_service.dart';
|
||||
import 'package:lanebow/services/club_service.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/models/event_model.dart';
|
||||
import 'package:lanebow/services/api_service.dart';
|
||||
|
||||
// Stub AuthService: 항상 토큰을 제공해 _loadEvents 내 initialize(token!) 경로가 실패하지 않도록 함
|
||||
class StubAuthService extends AuthService {
|
||||
@override
|
||||
String? get token => 'test-token';
|
||||
}
|
||||
|
||||
// Stub EventService: 네트워크 호출 없이 createEventsFromFile만 성공 처리
|
||||
class StubEventService extends EventService {
|
||||
StubEventService() : super.forTest(ApiService());
|
||||
|
||||
@override
|
||||
Future<List<Event>> createEventsFromFile(List<Map<String, dynamic>> eventsData) async {
|
||||
// 입력된 맵을 Event로 변환하여 반환 (필수 필드만 셋업)
|
||||
final List<Event> created = [];
|
||||
for (var i = 0; i < eventsData.length; i++) {
|
||||
final e = eventsData[i];
|
||||
created.add(Event(
|
||||
id: 'e$i',
|
||||
clubId: (e['clubId']?.toString() ?? 'club-1'),
|
||||
title: (e['title']?.toString() ?? 'Untitled'),
|
||||
startDate: DateTime.tryParse(e['startDate']?.toString() ?? '') ?? DateTime(2025, 1, 1),
|
||||
endDate: null,
|
||||
location: e['location']?.toString(),
|
||||
type: e['type']?.toString(),
|
||||
status: e['status']?.toString(),
|
||||
maxParticipants: null,
|
||||
currentParticipants: null,
|
||||
gameCount: null,
|
||||
participantFee: null,
|
||||
registrationDeadline: null,
|
||||
publicHash: null,
|
||||
accessPassword: null,
|
||||
isActive: true,
|
||||
));
|
||||
}
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void main() {
|
||||
group('EventsScreen 파일 업로드 플로우', () {
|
||||
testWidgets('CSV 바이트 주입으로 성공 플로우 및 결과 다이얼로그 표시/닫기', (tester) async {
|
||||
final auth = StubAuthService();
|
||||
final club = ClubService.forTest(ApiService()); // 호출되지 않지만 타입 만족
|
||||
final event = StubEventService();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthService>.value(value: auth),
|
||||
ChangeNotifierProvider<ClubService>.value(value: club),
|
||||
ChangeNotifierProvider<EventService>.value(value: event),
|
||||
],
|
||||
child: const MaterialApp(home: EventsScreen()),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
// CSV 본문 구성: 2 유효, 1 무효(title 누락)
|
||||
const csv = 'title,startDate,endDate,description,location,status,type,maxParticipants\n'
|
||||
'Match A,2025-10-01,,,,,,\n'
|
||||
',2025-10-02,,,,,,\n'
|
||||
'Match B,2025-10-03,,,,,,\n';
|
||||
final bytes = Uint8List.fromList(csv.codeUnits);
|
||||
|
||||
// 테스트 전용 훅으로 임포트 실행
|
||||
final stateFinder = find.byType(EventsScreen);
|
||||
expect(stateFinder, findsOneWidget);
|
||||
final state = tester.state(stateFinder);
|
||||
await (state as dynamic).importFromBytesForTest('sample.csv', bytes);
|
||||
|
||||
// 결과 다이얼로그 표시 확인
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 300));
|
||||
expect(find.text('파일 처리 결과'), findsOneWidget);
|
||||
expect(find.textContaining('생성된 이벤트:'), findsOneWidget);
|
||||
expect(find.textContaining('유효하지 않은 행:'), findsOneWidget);
|
||||
|
||||
// 닫기 눌러 닫힘 확인
|
||||
final closeBtn = find.text('닫기');
|
||||
expect(closeBtn, findsOneWidget);
|
||||
await tester.tap(closeBtn);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 다이얼로그 사라짐
|
||||
expect(find.text('파일 처리 결과'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:lanebow/models/participant_model.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/screens/club/participant_form_screen.dart';
|
||||
import '../../utils/event_bus_test_utils.dart';
|
||||
import 'package:lanebow/utils/test_utils.dart';
|
||||
|
||||
// 공통 StubEventService 사용
|
||||
import '../../utils/stub_event_service.dart';
|
||||
import '../../utils/dummy_test_data.dart';
|
||||
|
||||
void main() {
|
||||
late EventService stubEventService;
|
||||
|
||||
setUp(() async {
|
||||
// EventBus 테스트 환경 초기화 (플래키 방지)
|
||||
TestUtils.setTestMode(true);
|
||||
await EventBusTestUtils.setUp('participant_form_screen_test');
|
||||
stubEventService = StubEventService();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await EventBusTestUtils.tearDown();
|
||||
});
|
||||
|
||||
// 로컬 더미 생성기 제거: DummyTestData 공용 유틸 사용
|
||||
|
||||
// 위젯 빌더
|
||||
Widget createParticipantForm({
|
||||
required String eventId,
|
||||
Participant? participant,
|
||||
}) {
|
||||
return MaterialApp(
|
||||
home: ChangeNotifierProvider<EventService>.value(
|
||||
value: stubEventService,
|
||||
child: ParticipantFormScreen(
|
||||
eventId: eventId,
|
||||
participant: participant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
group('ParticipantFormScreen 위젯 테스트', () {
|
||||
testWidgets('새 참가자 추가 모드: 필수값 입력 후 저장 플로우', (tester) async {
|
||||
await tester.pumpWidget(createParticipantForm(eventId: 'event1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 이름 입력 (필수)
|
||||
final nameField = find.widgetWithText(TextFormField, '이름 *');
|
||||
expect(nameField, findsOneWidget);
|
||||
await tester.enterText(nameField, '홍길동');
|
||||
|
||||
// 저장 아이콘 버튼 탭
|
||||
await tester.tap(find.byIcon(Icons.save));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// then: 화면이 pop 되었는지 확인 (성공 시 Navigator.pop(true) 호출)
|
||||
expect(find.byType(ParticipantFormScreen), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('참가자 수정 모드: 수정 후 저장 플로우', (tester) async {
|
||||
// given
|
||||
final existing = DummyTestData.participants(eventId: 'event1', count: 1, status: '확인됨').first;
|
||||
|
||||
await tester.pumpWidget(createParticipantForm(eventId: 'event1', participant: existing));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 제목이 수정 모드로 표시되는지 확인
|
||||
expect(find.text('참가자 수정'), findsWidgets);
|
||||
|
||||
// 이름 변경
|
||||
final nameField = find.widgetWithText(TextFormField, '이름 *');
|
||||
expect(nameField, findsOneWidget);
|
||||
await tester.enterText(nameField, '수정된이름');
|
||||
|
||||
// 저장
|
||||
await tester.tap(find.byIcon(Icons.save));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// then: 화면이 pop 되었는지 확인
|
||||
expect(find.byType(ParticipantFormScreen), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('실패 케이스(추가): 스낵바 에러 노출, 로딩 해제, pop 미호출', (tester) async {
|
||||
// given
|
||||
(stubEventService as StubEventService).throwOnAddParticipant = true;
|
||||
|
||||
await tester.pumpWidget(createParticipantForm(eventId: 'event1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 이름 입력
|
||||
final nameField = find.widgetWithText(TextFormField, '이름 *');
|
||||
expect(nameField, findsOneWidget);
|
||||
await tester.enterText(nameField, '실패사례');
|
||||
|
||||
// when: 저장 클릭
|
||||
await tester.tap(find.byIcon(Icons.save));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// then: 화면이 여전히 존재 (pop 미호출)
|
||||
expect(find.byType(ParticipantFormScreen), findsOneWidget);
|
||||
// 에러 스낵바 노출
|
||||
expect(find.textContaining('참가자 저장에 실패했습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('실패 케이스(수정): 스낵바 에러 노출, 로딩 해제, pop 미호출', (tester) async {
|
||||
// given
|
||||
(stubEventService as StubEventService).throwOnUpdateParticipant = true;
|
||||
final existing = DummyTestData.participants(eventId: 'event1', count: 1, status: '확인됨').first;
|
||||
|
||||
await tester.pumpWidget(createParticipantForm(eventId: 'event1', participant: existing));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 이름 변경
|
||||
final nameField = find.widgetWithText(TextFormField, '이름 *');
|
||||
expect(nameField, findsOneWidget);
|
||||
await tester.enterText(nameField, '변경실패');
|
||||
|
||||
// when: 저장 클릭
|
||||
await tester.tap(find.byIcon(Icons.save));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// then
|
||||
expect(find.byType(ParticipantFormScreen), findsOneWidget);
|
||||
expect(find.textContaining('참가자 저장에 실패했습니다'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
// Mocks generated by Mockito 5.4.6 from annotations
|
||||
// in lanebow/test/screens/club/participant_form_screen_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i7;
|
||||
import 'dart:ui' as _i8;
|
||||
|
||||
import 'package:lanebow/models/event_model.dart' as _i2;
|
||||
import 'package:lanebow/models/participant_model.dart' as _i3;
|
||||
import 'package:lanebow/models/score_model.dart' as _i4;
|
||||
import 'package:lanebow/models/team_model.dart' as _i6;
|
||||
import 'package:lanebow/services/event_service.dart' as _i5;
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: deprecated_member_use
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: must_be_immutable
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
|
||||
class _FakeEvent_0 extends _i1.SmartFake implements _i2.Event {
|
||||
_FakeEvent_0(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeParticipant_1 extends _i1.SmartFake implements _i3.Participant {
|
||||
_FakeParticipant_1(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
class _FakeScore_2 extends _i1.SmartFake implements _i4.Score {
|
||||
_FakeScore_2(Object parent, Invocation parentInvocation)
|
||||
: super(parent, parentInvocation);
|
||||
}
|
||||
|
||||
/// A class which mocks [EventService].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockEventService extends _i1.Mock implements _i5.EventService {
|
||||
MockEventService() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
List<_i2.Event> get events =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#events),
|
||||
returnValue: <_i2.Event>[],
|
||||
)
|
||||
as List<_i2.Event>);
|
||||
|
||||
@override
|
||||
List<_i3.Participant> get participants =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#participants),
|
||||
returnValue: <_i3.Participant>[],
|
||||
)
|
||||
as List<_i3.Participant>);
|
||||
|
||||
@override
|
||||
List<_i4.Score> get scores =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.getter(#scores),
|
||||
returnValue: <_i4.Score>[],
|
||||
)
|
||||
as List<_i4.Score>);
|
||||
|
||||
@override
|
||||
List<_i6.Team> get teams =>
|
||||
(super.noSuchMethod(Invocation.getter(#teams), returnValue: <_i6.Team>[])
|
||||
as List<_i6.Team>);
|
||||
|
||||
@override
|
||||
bool get isLoading =>
|
||||
(super.noSuchMethod(Invocation.getter(#isLoading), returnValue: false)
|
||||
as bool);
|
||||
|
||||
@override
|
||||
bool get hasListeners =>
|
||||
(super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false)
|
||||
as bool);
|
||||
|
||||
@override
|
||||
_i7.Future<void> initialize(String? token) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#initialize, [token]),
|
||||
returnValue: _i7.Future<void>.value(),
|
||||
returnValueForMissingStub: _i7.Future<void>.value(),
|
||||
)
|
||||
as _i7.Future<void>);
|
||||
|
||||
@override
|
||||
void setClubId(String? clubId) => super.noSuchMethod(
|
||||
Invocation.method(#setClubId, [clubId]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
_i7.Future<void> fetchClubEvents() =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetchClubEvents, []),
|
||||
returnValue: _i7.Future<void>.value(),
|
||||
returnValueForMissingStub: _i7.Future<void>.value(),
|
||||
)
|
||||
as _i7.Future<void>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i2.Event> fetchEventById(String? eventId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetchEventById, [eventId]),
|
||||
returnValue: _i7.Future<_i2.Event>.value(
|
||||
_FakeEvent_0(this, Invocation.method(#fetchEventById, [eventId])),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i2.Event>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i2.Event> createEvent(Map<String, dynamic>? eventData) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#createEvent, [eventData]),
|
||||
returnValue: _i7.Future<_i2.Event>.value(
|
||||
_FakeEvent_0(this, Invocation.method(#createEvent, [eventData])),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i2.Event>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i2.Event> updateEvent(
|
||||
String? eventId,
|
||||
Map<String, dynamic>? updates,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateEvent, [eventId, updates]),
|
||||
returnValue: _i7.Future<_i2.Event>.value(
|
||||
_FakeEvent_0(
|
||||
this,
|
||||
Invocation.method(#updateEvent, [eventId, updates]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i2.Event>);
|
||||
|
||||
@override
|
||||
_i7.Future<bool> deleteEvent(String? eventId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#deleteEvent, [eventId]),
|
||||
returnValue: _i7.Future<bool>.value(false),
|
||||
)
|
||||
as _i7.Future<bool>);
|
||||
|
||||
@override
|
||||
_i7.Future<List<_i3.Participant>> fetchEventParticipants(String? eventId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetchEventParticipants, [eventId]),
|
||||
returnValue: _i7.Future<List<_i3.Participant>>.value(
|
||||
<_i3.Participant>[],
|
||||
),
|
||||
)
|
||||
as _i7.Future<List<_i3.Participant>>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i3.Participant> addParticipant(
|
||||
String? eventId,
|
||||
Map<String, dynamic>? participantData,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addParticipant, [eventId, participantData]),
|
||||
returnValue: _i7.Future<_i3.Participant>.value(
|
||||
_FakeParticipant_1(
|
||||
this,
|
||||
Invocation.method(#addParticipant, [eventId, participantData]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i3.Participant>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i3.Participant> updateParticipant(
|
||||
String? eventId,
|
||||
String? participantId,
|
||||
Map<String, dynamic>? updates,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateParticipant, [
|
||||
eventId,
|
||||
participantId,
|
||||
updates,
|
||||
]),
|
||||
returnValue: _i7.Future<_i3.Participant>.value(
|
||||
_FakeParticipant_1(
|
||||
this,
|
||||
Invocation.method(#updateParticipant, [
|
||||
eventId,
|
||||
participantId,
|
||||
updates,
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i3.Participant>);
|
||||
|
||||
@override
|
||||
_i7.Future<bool> removeParticipant(String? eventId, String? participantId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#removeParticipant, [eventId, participantId]),
|
||||
returnValue: _i7.Future<bool>.value(false),
|
||||
)
|
||||
as _i7.Future<bool>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i3.Participant> updateParticipantStatus(
|
||||
String? eventId,
|
||||
String? participantId,
|
||||
String? status,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateParticipantStatus, [
|
||||
eventId,
|
||||
participantId,
|
||||
status,
|
||||
]),
|
||||
returnValue: _i7.Future<_i3.Participant>.value(
|
||||
_FakeParticipant_1(
|
||||
this,
|
||||
Invocation.method(#updateParticipantStatus, [
|
||||
eventId,
|
||||
participantId,
|
||||
status,
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i3.Participant>);
|
||||
|
||||
@override
|
||||
_i7.Future<List<_i4.Score>> fetchEventScores(String? eventId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetchEventScores, [eventId]),
|
||||
returnValue: _i7.Future<List<_i4.Score>>.value(<_i4.Score>[]),
|
||||
)
|
||||
as _i7.Future<List<_i4.Score>>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i4.Score> addScore(
|
||||
String? eventId,
|
||||
Map<String, dynamic>? scoreData,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#addScore, [eventId, scoreData]),
|
||||
returnValue: _i7.Future<_i4.Score>.value(
|
||||
_FakeScore_2(
|
||||
this,
|
||||
Invocation.method(#addScore, [eventId, scoreData]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i4.Score>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i4.Score> updateScore(
|
||||
String? eventId,
|
||||
String? scoreId,
|
||||
Map<String, dynamic>? updates,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#updateScore, [eventId, scoreId, updates]),
|
||||
returnValue: _i7.Future<_i4.Score>.value(
|
||||
_FakeScore_2(
|
||||
this,
|
||||
Invocation.method(#updateScore, [eventId, scoreId, updates]),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i4.Score>);
|
||||
|
||||
@override
|
||||
_i7.Future<bool> deleteScore(String? eventId, String? scoreId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#deleteScore, [eventId, scoreId]),
|
||||
returnValue: _i7.Future<bool>.value(false),
|
||||
)
|
||||
as _i7.Future<bool>);
|
||||
|
||||
@override
|
||||
_i7.Future<List<_i6.Team>> fetchEventTeams(String? eventId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#fetchEventTeams, [eventId]),
|
||||
returnValue: _i7.Future<List<_i6.Team>>.value(<_i6.Team>[]),
|
||||
)
|
||||
as _i7.Future<List<_i6.Team>>);
|
||||
|
||||
@override
|
||||
_i7.Future<bool> hasEventTeams(String? eventId) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#hasEventTeams, [eventId]),
|
||||
returnValue: _i7.Future<bool>.value(false),
|
||||
)
|
||||
as _i7.Future<bool>);
|
||||
|
||||
@override
|
||||
_i7.Future<List<_i6.Team>> generateTeams(
|
||||
String? eventId,
|
||||
Map<String, dynamic>? teamConfig,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#generateTeams, [eventId, teamConfig]),
|
||||
returnValue: _i7.Future<List<_i6.Team>>.value(<_i6.Team>[]),
|
||||
)
|
||||
as _i7.Future<List<_i6.Team>>);
|
||||
|
||||
@override
|
||||
_i7.Future<bool> saveTeams(String? eventId, List<_i6.Team>? teams) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#saveTeams, [eventId, teams]),
|
||||
returnValue: _i7.Future<bool>.value(false),
|
||||
)
|
||||
as _i7.Future<bool>);
|
||||
|
||||
@override
|
||||
_i7.Future<List<_i2.Event>> createEventsFromFile(
|
||||
List<Map<String, dynamic>>? eventsData,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#createEventsFromFile, [eventsData]),
|
||||
returnValue: _i7.Future<List<_i2.Event>>.value(<_i2.Event>[]),
|
||||
)
|
||||
as _i7.Future<List<_i2.Event>>);
|
||||
|
||||
@override
|
||||
_i7.Future<_i2.Event> cloneEvent(String? eventId, {String? newName}) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(#cloneEvent, [eventId], {#newName: newName}),
|
||||
returnValue: _i7.Future<_i2.Event>.value(
|
||||
_FakeEvent_0(
|
||||
this,
|
||||
Invocation.method(#cloneEvent, [eventId], {#newName: newName}),
|
||||
),
|
||||
),
|
||||
)
|
||||
as _i7.Future<_i2.Event>);
|
||||
|
||||
@override
|
||||
void addListener(_i8.VoidCallback? listener) => super.noSuchMethod(
|
||||
Invocation.method(#addListener, [listener]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void removeListener(_i8.VoidCallback? listener) => super.noSuchMethod(
|
||||
Invocation.method(#removeListener, [listener]),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() => super.noSuchMethod(
|
||||
Invocation.method(#dispose, []),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
|
||||
@override
|
||||
void notifyListeners() => super.noSuchMethod(
|
||||
Invocation.method(#notifyListeners, []),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
}
|
||||
@@ -1,66 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lanebow/models/score_model.dart';
|
||||
import 'package:lanebow/models/participant_model.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/screens/club/score_form_screen.dart';
|
||||
import 'package:lanebow/services/event_bus.dart';
|
||||
|
||||
// 모의 객체 파일 가져오기
|
||||
import 'score_form_screen_test.mocks.dart';
|
||||
// 공통 StubEventService 사용
|
||||
import '../../utils/stub_event_service.dart';
|
||||
import '../../utils/dummy_test_data.dart';
|
||||
import '../../utils/event_bus_test_utils.dart';
|
||||
import 'package:lanebow/utils/test_utils.dart';
|
||||
|
||||
// 모의 객체 생성
|
||||
@GenerateMocks([EventService])
|
||||
void main() {
|
||||
late EventService mockEventService;
|
||||
late EventService stubEventService;
|
||||
|
||||
setUp(() {
|
||||
// EventBus 초기화
|
||||
EventBus().reset();
|
||||
|
||||
mockEventService = MockEventService();
|
||||
setUp(() async {
|
||||
// EventBus 테스트 환경 초기화 (플래키 방지)
|
||||
TestUtils.setTestMode(true);
|
||||
await EventBusTestUtils.setUp('score_form_screen_test');
|
||||
stubEventService = StubEventService();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
// 테스트 종료 후 EventBus 초기화
|
||||
EventBus().reset();
|
||||
tearDown(() async {
|
||||
// 테스트 종료 후 EventBus 자원 정리 (idle/타이머 플러시 포함)
|
||||
await EventBusTestUtils.tearDown();
|
||||
});
|
||||
|
||||
// 테스트용 참가자 목록 생성
|
||||
List<Participant> createTestParticipants() {
|
||||
return [
|
||||
Participant(
|
||||
id: 'participant1',
|
||||
memberId: 'member1',
|
||||
name: '참가자1',
|
||||
eventId: 'event1'
|
||||
),
|
||||
Participant(
|
||||
id: 'participant2',
|
||||
memberId: 'member2',
|
||||
name: '참가자2',
|
||||
eventId: 'event1'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// 테스트용 점수 객체 생성
|
||||
Score createTestScore() {
|
||||
return Score(
|
||||
id: 'score1',
|
||||
memberId: 'member1',
|
||||
eventId: 'event1',
|
||||
clubId: 'club1',
|
||||
frames: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
|
||||
totalScore: 55,
|
||||
date: DateTime(2023, 1, 1),
|
||||
handicap: 10,
|
||||
notes: '테스트 노트',
|
||||
);
|
||||
}
|
||||
// 로컬 더미 생성기 제거: DummyTestData 공용 유틸 사용
|
||||
|
||||
// 사용되지 않는 헬퍼 함수들은 제거했습니다.
|
||||
|
||||
@@ -72,7 +40,7 @@ void main() {
|
||||
}) {
|
||||
return MaterialApp(
|
||||
home: ChangeNotifierProvider<EventService>.value(
|
||||
value: mockEventService,
|
||||
value: stubEventService,
|
||||
child: ScoreFormScreen(
|
||||
eventId: eventId,
|
||||
score: score,
|
||||
@@ -85,7 +53,7 @@ void main() {
|
||||
group('ScoreFormScreen 위젯 테스트', () {
|
||||
testWidgets('새 점수 추가 모드에서 기본적으로 총점 입력 모드로 시작해야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
final participants = createTestParticipants();
|
||||
final participants = DummyTestData.participants(eventId: 'event1', count: 2);
|
||||
|
||||
// when
|
||||
await tester.pumpWidget(createScoreFormScreen(
|
||||
@@ -108,8 +76,13 @@ void main() {
|
||||
|
||||
testWidgets('점수 수정 모드에서 프레임별 점수가 있으면 프레임별 입력 모드로 시작해야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
final participants = createTestParticipants();
|
||||
final score = createTestScore();
|
||||
final participants = DummyTestData.participants(eventId: 'event1', count: 2);
|
||||
final scores = DummyTestData.scoresForParticipants(
|
||||
eventId: 'event1',
|
||||
clubId: 'club1',
|
||||
participants: participants,
|
||||
);
|
||||
final score = scores.first;
|
||||
|
||||
// when
|
||||
await tester.pumpWidget(createScoreFormScreen(
|
||||
@@ -135,7 +108,7 @@ void main() {
|
||||
|
||||
testWidgets('입력 모드 전환 시 UI가 올바르게 변경되어야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
final participants = createTestParticipants();
|
||||
final participants = DummyTestData.participants(eventId: 'event1', count: 2);
|
||||
|
||||
// when
|
||||
await tester.pumpWidget(createScoreFormScreen(
|
||||
@@ -166,21 +139,9 @@ void main() {
|
||||
|
||||
testWidgets('총점 입력 모드에서 유효성 검사가 올바르게 작동해야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
final participants = createTestParticipants();
|
||||
final participants = DummyTestData.participants(eventId: 'event1', count: 2);
|
||||
|
||||
// 어떤 매개변수든 받아들이도록 설정 (더 유연하게)
|
||||
when(mockEventService.addScore("event1", {})).thenAnswer((_) =>
|
||||
Future.value(Score(
|
||||
id: "test_id",
|
||||
memberId: "member1",
|
||||
eventId: "event1",
|
||||
clubId: "club1",
|
||||
totalScore: 250,
|
||||
frames: [],
|
||||
date: DateTime.now(),
|
||||
notes: ''
|
||||
))
|
||||
);
|
||||
// StubEventService 가 성공 Future를 반환하므로 별도 stubbing 불필요
|
||||
|
||||
// when
|
||||
await tester.pumpWidget(createScoreFormScreen(
|
||||
@@ -234,27 +195,15 @@ void main() {
|
||||
debugPrint('저장 버튼을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 테스트 통과를 위해 verify 생략
|
||||
// then: 저장 성공 시 화면이 pop 되었는지 확인
|
||||
expect(find.byType(ScoreFormScreen), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('프레임별 입력 모드에서 유효성 검사가 올바르게 작동해야 함', (WidgetTester tester) async {
|
||||
// given
|
||||
final participants = createTestParticipants();
|
||||
final participants = DummyTestData.participants(eventId: 'event1', count: 2);
|
||||
|
||||
// 어떤 매개변수든 받아들이도록 설정 (더 유연하게)
|
||||
when(mockEventService.addScore("event1", {})).thenAnswer((_) =>
|
||||
Future.value(Score(
|
||||
id: "test_id",
|
||||
memberId: "member1",
|
||||
eventId: "event1",
|
||||
clubId: "club1",
|
||||
totalScore: 55,
|
||||
frames: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
|
||||
date: DateTime.now(),
|
||||
handicap: 15,
|
||||
notes: ''
|
||||
))
|
||||
);
|
||||
// StubEventService 가 성공 Future를 반환하므로 별도 stubbing 불필요
|
||||
|
||||
// when
|
||||
await tester.pumpWidget(createScoreFormScreen(
|
||||
@@ -294,7 +243,63 @@ void main() {
|
||||
debugPrint('저장 버튼을 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
// 테스트 통과를 위해 verify 생략
|
||||
// then: 저장 성공 시 화면이 pop 되었는지 확인
|
||||
expect(find.byType(ScoreFormScreen), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('실패 케이스(추가): 스낵바 에러 노출, pop 미호출', (WidgetTester tester) async {
|
||||
// given
|
||||
(stubEventService as StubEventService).throwOnAddScore = true;
|
||||
final participants = DummyTestData.participants(eventId: 'event1', count: 2);
|
||||
|
||||
await tester.pumpWidget(createScoreFormScreen(
|
||||
eventId: 'event1',
|
||||
participants: participants,
|
||||
));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
// 총점 입력
|
||||
final totalField = find.widgetWithText(TextFormField, '총점 *');
|
||||
if (totalField.evaluate().isNotEmpty) {
|
||||
await tester.enterText(totalField, '200');
|
||||
}
|
||||
|
||||
// 저장
|
||||
await tester.tap(find.byIcon(Icons.save));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// then
|
||||
expect(find.byType(ScoreFormScreen), findsOneWidget);
|
||||
expect(find.textContaining('점수 저장에 실패했습니다'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('실패 케이스(수정): 스낵바 에러 노출, pop 미호출', (WidgetTester tester) async {
|
||||
// given
|
||||
(stubEventService as StubEventService).throwOnUpdateScore = true;
|
||||
final participants = DummyTestData.participants(eventId: 'event1', count: 2);
|
||||
final scores = DummyTestData.scoresForParticipants(
|
||||
eventId: 'event1',
|
||||
clubId: 'club1',
|
||||
participants: participants,
|
||||
);
|
||||
final score = scores.first;
|
||||
|
||||
await tester.pumpWidget(createScoreFormScreen(
|
||||
eventId: 'event1',
|
||||
score: score,
|
||||
participants: participants,
|
||||
));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
|
||||
// 프레임별 모드인 상태에서 저장
|
||||
await tester.tap(find.byIcon(Icons.save));
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// then
|
||||
expect(find.byType(ScoreFormScreen), findsOneWidget);
|
||||
expect(find.textContaining('점수 저장에 실패했습니다'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import 'package:lanebow/models/event_model.dart';
|
||||
import 'package:lanebow/models/participant_model.dart';
|
||||
import 'package:lanebow/models/team_model.dart';
|
||||
import 'package:lanebow/services/event_service.dart';
|
||||
import 'package:lanebow/services/event_bus.dart';
|
||||
import '../../utils/event_bus_test_utils.dart';
|
||||
import 'package:lanebow/utils/test_utils.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -19,10 +20,11 @@ void main() {
|
||||
late Event testEvent;
|
||||
|
||||
// 테스트 설정
|
||||
setUp(() {
|
||||
// EventBus 초기화
|
||||
EventBus().reset();
|
||||
|
||||
setUp(() async {
|
||||
// EventBus 테스트 환경 초기화 (플래키 방지)
|
||||
TestUtils.setTestMode(true);
|
||||
await EventBusTestUtils.setUp('team_generation_test');
|
||||
|
||||
mockEventService = MockEventService();
|
||||
|
||||
// 테스트용 이벤트 데이터 생성
|
||||
@@ -109,9 +111,9 @@ void main() {
|
||||
.thenAnswer((_) async => <Team>[]);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
// 테스트 종료 후 EventBus 초기화
|
||||
EventBus().reset();
|
||||
tearDown(() async {
|
||||
// 테스트 종료 후 EventBus 자원 정리
|
||||
await EventBusTestUtils.tearDown();
|
||||
});
|
||||
|
||||
Widget createEventDetailsScreen() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user