import 'dart:async'; import 'package:fancake/app.dart'; import 'package:fancake/core/data/repositories.dart'; import 'package:fancake/core/firebase/firebase_boot.dart'; import 'package:fancake/features/auth/auth.dart'; import 'package:fancake/features/auth/auth_repository.dart'; import 'package:fancake/features/auth/login_screen.dart'; import 'package:fancake/features/auth/onboarding_data.dart'; import 'package:fancake/features/auth/onboarding_screens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// 온보딩 6단계(10/3 통합회신 1항) · 「로그인」 버튼 신규 계정의 약관 우회 차단(E-05). void main() { setUp(() { FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '온보딩 테스트'); SharedPreferences.setMockInitialValues({}); }); Future<(AuthRepository, SampleOnboardingRepository)> boot( WidgetTester tester, { required bool needs, bool intro = false, Duration checkDelay = Duration.zero, }) async { tester.view.physicalSize = const Size(1080, 2400); tester.view.devicePixelRatio = 2; addTearDown(tester.view.reset); final repos = Repositories(live: false); final auth = AuthRepository( providers: [_Provider()], releaseMode: true, needsOnboarding: (_) => Future.delayed(checkDelay, () => needs), introPending: intro, ); addTearDown(auth.dispose); await tester.pumpWidget(FancakeApp(auth: auth, repositories: repos)); await tester.pumpAndSettle(); return (auth, repos.onboarding as SampleOnboardingRepository); } testWidgets('「로그인」으로 들어온 신규 계정도 약관부터 — 셸로 가지 않는다', (tester) async { final (auth, _) = await boot(tester, needs: true); await auth.signIn(SocialProvider.google); await tester.pumpAndSettle(); expect(auth.onboarding, OnboardingGate.required); expect(find.byType(NavigationBar), findsNothing); expect(find.text('개인정보 수집·이용 및 처리방침 동의 (필수)'), findsOneWidget); }); testWidgets('약관 동의가 있는 계정은 바로 셸', (tester) async { final (auth, _) = await boot(tester, needs: false); await auth.signIn(SocialProvider.google); await tester.pumpAndSettle(); expect(find.byType(NavigationBar), findsOneWidget); }); testWidgets('동의한 계정은 셸로 가는 동안 약관 화면이 한 프레임도 그려지지 않는다 — 10/6 콜드 스타트 깜빡임', (tester) async { // 서버 판정이 몇 프레임 걸리는 실기기처럼 늦춘다. final (auth, _) = await boot( tester, needs: false, checkDelay: const Duration(milliseconds: 200), ); unawaited(auth.signIn(SocialProvider.google)); for (var i = 0; i < 60; i++) { await tester.pump(const Duration(milliseconds: 16)); expect(find.byType(OnboardingFlow), findsNothing); } await tester.pumpAndSettle(); expect(find.byType(NavigationBar), findsOneWidget); }); testWidgets('약관 → 프로필 → 관심 아티스트 → 행성 가입 → 권한 → 가입 완료 → 셸', (tester) async { final (auth, repo) = await boot(tester, needs: true); await auth.signIn(SocialProvider.google); await tester.pumpAndSettle(); // ② 약관 — 필수만 동의(마케팅 미동의). for (final label in [ '서비스 이용약관 (필수)', '개인정보 수집·이용 및 처리방침 동의 (필수)', '위치기반 서비스 이용약관 (필수)', '만 14세 이상입니다. (필수)', ]) { await tester.tap(find.text(label)); } await tester.pump(); await tester.tap(find.text('완료')); await tester.pumpAndSettle(); expect(repo.agreed, {'service', 'privacy', 'location', 'age'}); // ③ 프로필 await tester.enterText(find.byType(TextField).first, '우주팬'); await tester.enterText(find.byType(TextField).last, 'ABCDEFGHJK'); await tester.pump(); await tester.tap(find.text('다음')); await tester.pumpAndSettle(); expect(repo.profile?.nickname, '우주팬'); expect(repo.profile?.country, isNotEmpty); expect(repo.referral, 'ABCDEFGHJK'); // ④ 관심 아티스트 — 최소 1개 전에는 다음이 잠겨 있다. final next = find.widgetWithText(FilledButton, '다음'); expect(tester.widget(next).onPressed, isNull); await tester.tap(find.text('NewJeans')); await tester.tap(find.text('BTS')); await tester.pump(); await tester.tap(next); await tester.pumpAndSettle(); // ⑤ 행성 가입 — 고른 순서(첫 번째가 대표). await tester.tap(find.text('선택한 행성 가입하기')); await tester.pumpAndSettle(); expect(repo.joined, ['newjeans', 'bts']); // ⑥ 권한 안내 → 가입 완료(182:29344) → 셸 expect(find.text('앱 권한 안내'), findsOneWidget); await tester.tap(find.text('계속')); await tester.pumpAndSettle(); expect(find.text('회원가입이 완료되었습니다'), findsOneWidget); await tester.tap(find.text('시작')); await tester.pumpAndSettle(); expect(find.byType(NavigationBar), findsOneWidget); expect( (await SharedPreferences.getInstance()).getBool('coach.pending'), isNotNull, ); }); testWidgets('프로필 국가 — 사본 국가 리스트 모달(178:23489) 대로 국가명 · 단수 선택 · 「선택」 후 적용', ( tester, ) async { final (auth, repo) = await boot(tester, needs: true); await auth.signIn(SocialProvider.google); await tester.pumpAndSettle(); for (final label in [ '서비스 이용약관 (필수)', '개인정보 수집·이용 및 처리방침 동의 (필수)', '위치기반 서비스 이용약관 (필수)', '만 14세 이상입니다. (필수)', ]) { await tester.tap(find.text(label)); } await tester.pump(); await tester.tap(find.text('완료')); await tester.pumpAndSettle(); await tester.tap(find.text('국가/지역').first); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.text('대한민국'), 300, scrollable: find.byType(Scrollable).last, ); await tester.tap(find.text('대한민국')); await tester.pump(); await tester.tap(find.widgetWithText(FilledButton, '선택')); await tester.pumpAndSettle(); expect(find.textContaining('대한민국'), findsOneWidget); await tester.enterText(find.byType(TextField).first, '우주팬'); await tester.pump(); await tester.tap(find.text('다음')); await tester.pumpAndSettle(); expect(repo.profile?.country, 'KR'); }); testWidgets('프로필 뒤로가기(511:86825) — 약관으로 돌아가 동의가 체크된 채 보이고 다시 기록하지 않는다', ( tester, ) async { final (auth, repo) = await boot(tester, needs: true); await auth.signIn(SocialProvider.google); await tester.pumpAndSettle(); for (final label in [ '서비스 이용약관 (필수)', '개인정보 수집·이용 및 처리방침 동의 (필수)', '위치기반 서비스 이용약관 (필수)', '만 14세 이상입니다. (필수)', ]) { await tester.tap(find.text(label)); } await tester.pump(); await tester.tap(find.text('완료')); await tester.pumpAndSettle(); final first = repo.agreed; await tester.tap(find.byType(BackButton)); await tester.pumpAndSettle(); // 필수 4항이 이미 체크돼 있어 「완료」가 바로 켜져 있다. final done = find.widgetWithText(FilledButton, '완료'); expect(tester.widget(done).onPressed, isNotNull); await tester.tap(done); await tester.pumpAndSettle(); expect(identical(repo.agreed, first), isTrue); expect(find.byType(BackButton), findsOneWidget); // ③ 프로필로 돌아왔다. }); testWidgets('가입 모드에서 받은 약관은 다시 묻지 않고 기록만 한다', (tester) async { final (auth, repo) = await boot(tester, needs: true); auth.pendingConsent = { 'service', 'privacy', 'location', 'age', 'marketing', }; await auth.signIn(SocialProvider.google, signUp: true); await tester.pumpAndSettle(); expect(repo.agreed, contains('marketing')); expect(find.text('프로필을 설정해 주세요'), findsOneWidget); }); testWidgets('첫 실행은 소개 3장 — 건너뛰기를 눌러도 로그인 화면', (tester) async { final (auth, _) = await boot(tester, needs: false, intro: true); expect(find.text('우리 팬덤의 행성을 키워요'), findsOneWidget); await tester.tap(find.text('건너뛰기')); await tester.pumpAndSettle(); expect(auth.introPending, isFalse); expect(find.byKey(loginLogoKey), findsOneWidget); }); } final class _Provider implements SocialAuthProvider { @override AuthProviderKind get provider => SocialProvider.google; @override bool get isLive => true; @override List get missingConfig => const []; @override Future signIn() async => const AuthSucceeded( AuthAccount(uid: 'onb-user', provider: SocialProvider.google), ); @override Future signOut() async {} }