Files
fanuzu/app/test/onboarding_test.dart
T

381 lines
15 KiB
Dart

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/core/i18n/app_locale.dart';
import 'package:fancake/core/ui/app_dialog.dart';
import 'package:fancake/features/planet/planet_data.dart' show countryName;
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/services.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);
// Q-E1 · R02 — 위치기반 약관은 가입 때 받지 않고, 개인정보처리방침은 체크 칸 없는 고지 · 보기 링크다.
expect(find.textContaining('위치기반'), findsNothing);
expect(find.text('개인정보처리방침'), findsNothing);
await tester.tap(find.byKey(const ValueKey('privacy-policy-link')));
await tester.pumpAndSettle();
expect(find.text('개인정보처리방침'), findsOneWidget);
});
testWidgets('개인정보처리방침 보기는 약관 문구가 아닌 처리방침 문구를 보여 준다', (tester) async {
final (auth, _) = await boot(tester, needs: true);
await auth.signIn(SocialProvider.google);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const ValueKey('privacy-policy-link')));
await tester.pumpAndSettle();
final body = tester
.widget<Text>(
find.descendant(
of: find.byType(AppDialog),
matching: find.textContaining('임시 문구'),
),
)
.data!;
expect(body, contains('개인정보처리방침'));
expect(body, isNot(contains('서비스 이용약관')));
});
testWidgets('개인정보처리방침 고지는 iPhone 11 높이에서 스크롤 없이 하단 버튼 위에 보이고 체크 칸이 없다', (
tester,
) async {
final (auth, _) = await boot(tester, needs: true);
// iPhone 11 — 414x896pt · 상단 48 · 하단 34.
tester.view.physicalSize = const Size(828, 1792);
tester.view.padding = const FakeViewPadding(top: 96, bottom: 68);
await auth.signIn(SocialProvider.google);
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 {
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', '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<FilledButton>(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.ancestor(of: find.text('거주 국가/지역'), matching: find.byType(InkWell)),
);
await tester.pumpAndSettle();
expect(find.text('거주 국가/지역 선택'), findsOneWidget);
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<FilledButton>(done).onPressed, isNotNull);
await tester.tap(done);
await tester.pumpAndSettle();
expect(identical(repo.agreed, first), isTrue);
expect(find.byType(BackButton), findsOneWidget); // ③ 프로필로 돌아왔다.
});
testWidgets('추천인 코드(C-119) — 귀속 뒤 돌아와도 다시 부르지 않고, 다른 코드는 511:86845 문구로 거절', (
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.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.referral, 'ABCDEFGHJK');
// ④ → ③ : 귀속한 코드가 채워져 있고 그대로 「다음」이면 통과(두 번째 호출 없음).
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('ABCDEFGHJK'), findsOneWidget);
// 목 저장소는 저장한 닉네임을 돌려주지 않는다 — 다시 넣는다.
await tester.enterText(find.byType(TextField).first, '우주팬');
await tester.pump();
await tester.tap(find.text('다음'));
await tester.pumpAndSettle();
expect(find.text('NewJeans'), findsOneWidget); // ④ 로 넘어갔다.
// 다른 코드로 바꾸면 서버가 1인 1회로 거절 → Figma 오류 문구, 단계는 그대로.
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, '우주팬');
await tester.enterText(find.byType(TextField).last, 'ZZZZZZZZZZ');
await tester.pump();
await tester.tap(find.text('다음'));
await tester.pumpAndSettle();
expect(find.text('추천인 코드를 확인해 주세요.'), findsOneWidget);
expect(repo.referral, 'ABCDEFGHJK');
});
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); // ③ 프로필
expect(find.text('회원가입'), findsOneWidget); // 511:86825 머리줄 가운데 타이틀
});
test('국가명은 OS 이름 · OS 정렬 순서 — 이름 없는 나라도 코드로 보이지 않는다', () async {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('kr.co.fanuzu.app/locale');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
String? asked;
messenger.setMockMethodCallHandler(channel, (call) async {
asked = (call.arguments as Map)['locale'] as String;
return [
['GH', '가나'],
['AD', '안도라'],
['KR', '대한민국'],
];
});
addTearDown(() {
messenger.setMockMethodCallHandler(channel, null);
platformCountryNames = const {};
});
await loadCountryNames(const Locale('ko'));
expect(asked, 'ko');
expect(countryName('AD'), '안도라');
expect(sortedCountries().take(3), ['GH', 'AD', 'KR']);
expect(sortedCountries().length, isoCountries.length);
});
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<String> get missingConfig => const [];
@override
Future<AuthOutcome> signIn() async => const AuthSucceeded(
AuthAccount(uid: 'onb-user', provider: SocialProvider.google),
);
@override
Future<void> signOut() async {}
}