import 'dart:async'; import 'package:fancake/app.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/signup_screens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; /// 가입 약관 — 동의 기록이 없는 계정은 HOME 대신 약관으로 가고, 동의를 기록한 뒤에만 HOME 에 들어간다. /// 소셜 로그인 진행 중에는 화면 전체를 막고 진행 표시를 띄운다. void main() { setUp(() { FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '가입 약관 위젯 테스트'); }); const newUser = AuthAccount( uid: 'new-user', provider: AuthProviderKind.kakao, ); testWidgets('로그인 진행 중 로딩 · 중복 입력 막음 → 동의 없는 새 계정은 약관 → 필수 동의 → HOME', ( tester, ) async { final signIn = Completer(); final provider = _Provider(AuthProviderKind.kakao, () => signIn.future); Set? recorded; final auth = AuthRepository( providers: [provider], releaseMode: false, needsTerms: (_) async => recorded == null, recordTerms: (agreed) async => recorded = agreed, ); await tester.pumpWidget(FancakeApp(auth: auth)); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, '로그인')); await tester.pumpAndSettle(); final kakao = find.text(providerButtonLabel(AuthProviderKind.kakao)); await tester.tap(kakao); await tester.pump(); expect(find.byType(BlockingProgress), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsOneWidget); await tester.tap(kakao, warnIfMissed: false); await tester.pump(); expect(provider.calls, 1); signIn.complete(const AuthSucceeded(newUser)); await tester.pumpAndSettle(); expect(find.byType(NavigationBar), findsNothing); expect(find.text('이용 약관'), findsOneWidget); expect(find.text('위치기반 서비스 이용약관 (필수)'), findsNothing); final done = find.widgetWithText(FilledButton, '완료'); expect(tester.widget(done).onPressed, isNull); for (final label in [ '서비스 이용약관 (필수)', '개인정보 수집·이용 동의 (필수)', '만 14세 이상입니다. (필수)', ]) { await tester.scrollUntilVisible(find.text(label), 100); await tester.tap(find.text(label)); await tester.pump(); } // 개인정보처리방침은 체크 칸 없는 고지 · 보기 링크. final policy = find.byKey(const ValueKey('privacy-policy-link')); await tester.tap(policy); await tester.pumpAndSettle(); expect(find.text('개인정보처리방침'), findsOneWidget); await tester.tap(find.text('확인')); await tester.pumpAndSettle(); expect(tester.widget(done).onPressed, isNotNull); await tester.tap(done); await tester.pumpAndSettle(); expect(recorded, {'service', 'privacy', 'age'}); expect(find.byType(NavigationBar), findsOneWidget); expect(find.byType(BlockingProgress), findsNothing); }); testWidgets('개인정보처리방침 고지는 스크롤 없이 하단 버튼 위에 보이고 체크 칸이 없다', (tester) async { // iPhone 11 — 414x896pt · 상단 48 · 하단 34. tester.view.physicalSize = const Size(828, 1792); tester.view.devicePixelRatio = 2; tester.view.padding = const FakeViewPadding(top: 96, bottom: 68); addTearDown(tester.view.reset); await tester.pumpWidget(const MaterialApp(home: TermsScreen())); await tester.pumpAndSettle(); final notice = find.byKey(const ValueKey('privacy-policy-notice')); expect( find.descendant( of: notice, matching: find.text('개인정보 처리 내용은 개인정보처리방침에서 확인할 수 있습니다.'), ), findsOneWidget, ); final link = find.descendant(of: notice, matching: find.text('보기')); expect(link, findsOneWidget); expect( find.descendant(of: notice, matching: find.byType(ImageIcon)), findsNothing, ); // 화면 안 · 「이전」 버튼 위. expect(tester.getRect(link).bottom, lessThanOrEqualTo(896 - 34)); expect( tester.getRect(notice).bottom, lessThanOrEqualTo( tester.getRect(find.widgetWithText(FilledButton, '이전')).top, ), ); await tester.tap(link); await tester.pumpAndSettle(); expect(find.text('개인정보처리방침'), findsOneWidget); }); testWidgets('개인정보처리방침 보기는 약관 문구가 아닌 처리방침 문구를 보여 준다', (tester) async { await tester.pumpWidget(const MaterialApp(home: TermsScreen())); await tester.pumpAndSettle(); await tester.tap(find.byKey(const ValueKey('privacy-policy-link'))); await tester.pumpAndSettle(); final body = tester .widget( find.descendant( of: find.byType(AlertDialog), matching: find.textContaining('임시 문구'), ), ) .data!; expect(body, contains('개인정보처리방침')); expect(body, isNot(contains('서비스 이용약관'))); }); testWidgets('이미 동의한 계정은 약관 없이 HOME', (tester) async { final auth = AuthRepository( providers: [ _Provider( AuthProviderKind.google, () async => const AuthSucceeded(newUser), ), ], releaseMode: false, needsTerms: (_) async => false, recordTerms: (_) async => fail('기록하지 않는다'), ); await tester.pumpWidget(FancakeApp(auth: auth)); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, '로그인')); await tester.pumpAndSettle(); await tester.tap(find.text(providerButtonLabel(AuthProviderKind.google))); await tester.pumpAndSettle(); expect(find.byType(NavigationBar), findsOneWidget); }); testWidgets('가입 모드 — 인증 전에 받은 동의를 기록하고 약관을 다시 묻지 않는다', (tester) async { Set? recorded; final auth = AuthRepository( providers: [ _Provider( AuthProviderKind.line, () async => const AuthSucceeded(newUser), ), ], releaseMode: false, needsTerms: (_) async => recorded == null, recordTerms: (agreed) async => recorded = agreed, ); await tester.pumpWidget(FancakeApp(auth: auth)); await tester.pumpAndSettle(); await tester.tap(find.text('회원가입')); await tester.pumpAndSettle(); await tester.tap( find.text(providerButtonLabel(AuthProviderKind.line, signUp: true)), ); await tester.pumpAndSettle(); await tester.tap(find.text('전체 동의')); await tester.pump(); await tester.tap(find.widgetWithText(FilledButton, '완료')); await tester.pumpAndSettle(); expect(recorded, {'service', 'privacy', 'age', 'marketing'}); expect(find.byType(NavigationBar), findsOneWidget); }); } final class _Provider implements SocialAuthProvider { _Provider(this.provider, this._signIn); @override final AuthProviderKind provider; final Future Function() _signIn; int calls = 0; @override bool get isLive => true; @override List get missingConfig => const []; @override Future signIn() { calls += 1; return _signIn(); } @override Future signOut() async {} }