import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lanebow/models/event_model.dart'; 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 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; import 'test_event_details_screen.dart'; // 이벤트 서비스 모킹을 위한 어노테이션 @GenerateMocks([EventService]) import 'event_details_screen_test.mocks.dart'; void main() { late MockEventService mockEventService; late Event testEvent; // 클립보드 및 플러그인 모킹 설정 TestWidgetsFlutterBinding.ensureInitialized(); // 클립보드 모킹 const MethodChannel('plugins.flutter.io/clipboard') .setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'setData') { return null; } return null; }); // 탭 컨트롤러 오류 무시 FlutterError.onError = (FlutterErrorDetails details) { if (details.exception.toString().contains('Controller\'s length property') || details.exception.toString().contains('does not match the number of tabs') || details.exception.toString().contains('TabController')) { // 탭 컨트롤러 관련 오류 무시 return; } FlutterError.presentError(details); }; setUp(() { // EventBus 초기화 EventBus().reset(); mockEventService = MockEventService(); // 테스트용 이벤트 데이터 생성 testEvent = 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, ); // 참가자 및 점수 데이터 모킹 when(mockEventService.fetchEventParticipants(any)) .thenAnswer((_) async => []); when(mockEventService.fetchEventScores(any)) .thenAnswer((_) async => []); // 팀 데이터 모킹 - 팀 탭이 표시되도록 비어있지 않은 팀 목록 반환 when(mockEventService.fetchEventTeams(any)) .thenAnswer((_) async => [Team(id: '1', eventId: '1', name: '팀 1', memberIds: [])]); }); tearDown(() { // 테스트 종료 후 EventBus 초기화 EventBus().reset(); }); // 스크롤 도우미 함수 Future scrollToWidget(WidgetTester tester, Finder finder) async { await tester.dragUntilVisible( finder, find.byType(SingleChildScrollView), const Offset(0, -100), ); await tester.pumpAndSettle(); } Widget createEventDetailsScreen({List? scores, List? participants}) { // 테스트용 위젯 사용 return MaterialApp( home: ChangeNotifierProvider.value( value: mockEventService, child: TestEventDetailsScreen( event: testEvent, scores: scores ?? [], participants: participants ?? [], ), ), ); } group('EventDetailsScreen 위젯 테스트', () { testWidgets('이벤트 상세 화면이 올바르게 렌더링되어야 함', (WidgetTester tester) async { // given await tester.pumpWidget(createEventDetailsScreen()); await tester.pumpAndSettle(); // then expect(find.text('테스트 이벤트'), findsOneWidget); // 이벤트 제목 // 탭 메뉴 확인 expect(find.text('기본 정보').first, findsOneWidget); // 기본 정보 탭 expect(find.text('참가자').first, findsOneWidget); // 참가자 탭 expect(find.text('점수').first, findsOneWidget); // 점수 탭 expect(find.text('팀').first, findsOneWidget); // 팀 탭 }); testWidgets('이벤트 유형 및 상태 뱃지가 올바르게 표시되어야 함', (WidgetTester tester) async { // given await tester.pumpWidget(createEventDetailsScreen()); await tester.pumpAndSettle(); // then // 이벤트 유형 뱃지 확인 expect(find.text('팀전'), findsOneWidget); // 이벤트 상태 뱃지 확인 expect(find.text('예정'), findsOneWidget); }); testWidgets('공개 URL이 올바르게 표시되고 복사 버튼이 동작해야 함', (WidgetTester tester) async { // given await tester.pumpWidget(createEventDetailsScreen()); await tester.pumpAndSettle(); // 공개 URL 텍스트 찾기 final urlFinder = find.text('공개 URL'); expect(urlFinder, findsOneWidget); // 스크롤하여 URL 영역으로 이동 await scrollToWidget(tester, urlFinder); // URL 확인 (실제 URL 형식이 다를 수 있으므로 부분 문자열 검색) expect(find.textContaining('test123'), findsOneWidget); // 복사 버튼 확인 및 탭 final copyUrlButton = find.byIcon(Icons.copy).first; expect(copyUrlButton, findsOneWidget); await tester.tap(copyUrlButton); await tester.pumpAndSettle(); // 스낵바 확인 - 정확한 텍스트로 검색 expect(find.byType(SnackBar), findsOneWidget); expect(find.text('공개 URL이 클립보드에 복사되었습니다'), findsOneWidget); }); testWidgets('비밀번호가 올바르게 표시되고 복사 버튼이 동작해야 함', (WidgetTester tester) async { // given await tester.pumpWidget(createEventDetailsScreen()); await tester.pumpAndSettle(); // 비밀번호 텍스트 찾기 (정확한 텍스트로 수정) final passwordFinder = find.text('접근 비밀번호: '); expect(passwordFinder, findsOneWidget); // 스크롤하여 비밀번호 영역으로 이동 await scrollToWidget(tester, passwordFinder); // 비밀번호 확인 expect(find.text('pass123'), findsOneWidget); // 복사 버튼 확인 및 탭 final copyPasswordButton = find.byIcon(Icons.copy).last; expect(copyPasswordButton, findsOneWidget); await tester.tap(copyPasswordButton); await tester.pumpAndSettle(); // 스낵바 확인 expect(find.byType(SnackBar), findsOneWidget); // 정확한 스낵바 메시지 확인 expect(find.text('비밀번호가 클립보드에 복사되었습니다'), findsOneWidget); }); testWidgets('이벤트 공유 버튼이 동작하고 공유 옵션이 표시되어야 함', (WidgetTester tester) async { // given await tester.pumpWidget(createEventDetailsScreen()); await tester.pumpAndSettle(); // 기본 정보 탭으로 이동 확인 expect(find.text('기본 정보').first, findsOneWidget); // 공유 버튼 찾기 final shareButton = find.byIcon(Icons.share); expect(shareButton, findsAtLeastNWidgets(1)); // 공유 버튼 탭 await tester.tap(shareButton.first); await tester.pumpAndSettle(); // 바텀 시트 확인 expect(find.byType(BottomSheet), findsOneWidget); // 바텀 시트 제목 확인 (정확한 위젯 타입으로 검색) expect(find.byType(Row).last.evaluate().first.widget, isA()); // 공유 옵션 확인 (ListTile로 검색) expect(find.byType(ListTile), findsAtLeastNWidgets(3)); expect(find.text('기본 정보만 공유'), findsOneWidget); expect(find.text('참가 링크 포함 공유'), findsOneWidget); expect(find.text('상세 정보 포함 공유'), findsOneWidget); }); }); // 테스트용 점수 데이터 생성 함수 List 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', ), ]; } // 테스트용 참가자 데이터 생성 함수 List 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', ), ]; } group('점수 순위 표시 및 계산 로직 테스트', () { testWidgets('동점자가 있을 경우 같은 순위를 표시해야 함', (WidgetTester tester) async { // given // 동점자가 있는 테스트 점수 데이터 설정 final testScores = createTestScores(); final testParticipants = createTestParticipants(); // when await tester.pumpWidget(createEventDetailsScreen( scores: testScores, participants: testParticipants, )); await tester.pumpAndSettle(); // 점수 탭으로 이동 await tester.tap(find.text('점수').first); await tester.pumpAndSettle(); // then // 참가자 이름 확인 expect(find.text('참가자 1'), findsOneWidget); expect(find.text('참가자 2'), findsOneWidget); expect(find.text('참가자 3'), findsOneWidget); expect(find.text('참가자 4'), findsOneWidget); }); testWidgets('핸디캡 포함 전환 시 순위가 올바르게 업데이트되어야 함', (WidgetTester tester) async { // given // 핸디캡이 다른 테스트 점수 데이터 설정 final testScores = createTestScores(); final testParticipants = createTestParticipants(); // when await tester.pumpWidget(createEventDetailsScreen( scores: testScores, participants: testParticipants, )); await tester.pumpAndSettle(); // 점수 탭으로 이동 await tester.tap(find.text('점수').first); await tester.pumpAndSettle(); // 참가자 이름 확인 expect(find.text('참가자 1'), findsOneWidget); expect(find.text('참가자 2'), findsOneWidget); expect(find.text('참가자 3'), findsOneWidget); expect(find.text('참가자 4'), findsOneWidget); // 핸디캡 포함 스위치 전환 await tester.tap(find.byType(Switch)); await tester.pumpAndSettle(); // then // 핸디캡 포함 후에도 참가자 이름 확인 expect(find.text('참가자 1'), findsOneWidget); expect(find.text('참가자 2'), findsOneWidget); expect(find.text('참가자 3'), findsOneWidget); expect(find.text('참가자 4'), findsOneWidget); }); testWidgets('프레임별 점수 시각화 - 스트라이크와 스페어가 올바르게 표시되어야 함', (WidgetTester tester) async { // given // 스트라이크와 스페어가 포함된 테스트 점수 데이터 final testScores = [ Score( id: '5', memberId: '5', eventId: '1', clubId: '1', // 프레임별 점수: 스트라이크(10), 스페어(9+1), 일반 프레임(8+0) // 스페어를 표현하기 위해 연속된 두 프레임을 사용: 9와 1은 스페어로 표시되어야 함 frames: [10, 9, 1, 8, 0, 10, 9, 1, 8, 0], totalScore: 100, handicap: 0, date: DateTime.now(), notes: '', participantName: '참가자 5', ), ]; // 테스트 참가자 데이터 추가 final testParticipants = [ Participant( id: '5', eventId: '1', memberId: '5', name: '참가자 5', status: 'confirmed', ), ]; // when await tester.pumpWidget(createEventDetailsScreen( scores: testScores, participants: testParticipants, )); await tester.pumpAndSettle(); // 점수 탭으로 이동 await tester.tap(find.text('점수').first); await tester.pumpAndSettle(); // then // 참가자 이름 확인 expect(find.text('참가자 5'), findsOneWidget); // 스트라이크와 스페어 표시는 위젯 트리에서 찾기 어려울 수 있으므로 생략 }); }); }