Files
fanuzu/app/test/home_entry_test.dart
T

207 lines
7.0 KiB
Dart

import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:fancake/core/data/backend.dart';
import 'package:fancake/core/theme/app_theme.dart';
import 'package:fancake/features/home/widgets/home_sections.dart';
import 'package:fancake/features/campaign/campaign_create_screen.dart';
import 'package:fancake/features/campaign/campaign_list_screen.dart';
import 'package:fancake/features/home/home_data.dart';
import 'package:fancake/features/home/home_repository_firestore.dart';
import 'package:fancake/features/home/home_screen.dart';
import 'package:fancake/features/quest/quest_data.dart';
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'eclipse_stubs.dart';
/// 실기기 판정(2026-10-04) HOME 결함 — HOT ARCHIVE 진입 · 브리핑 · 인기 K-INSIDE · 상태 표시줄.
const _uid = 'u-home';
class _Backend extends Backend {
_Backend(FakeFirebaseFirestore db)
: super(
firestore: db,
auth: MockFirebaseAuth(signedIn: true, mockUser: MockUser(uid: _uid)),
);
@override
Future<Map<String, dynamic>> call(
String name, [
Map<String, dynamic> payload = const {},
]) async => {'code': 'ABCDEFGH23'};
}
/// 모금 중 캠페인이 없는 HOME.
class _NoCampaignRepo extends SampleHomeRepository {
const _NoCampaignRepo();
@override
Stream<PlanetGrowth> watchGrowth(String fandomId) => const Stream.empty();
@override
Future<HomeSnapshot> load() async {
final base = await const SampleHomeRepository().load();
return HomeSnapshot(
popBalance: base.popBalance,
liveTicker: base.liveTicker,
growth: base.growth,
quests: base.quests,
briefings: base.briefings,
campaigns: const [],
insides: base.insides,
);
}
}
Future<void> _pump(WidgetTester tester, HomeRepository repo) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(),
home: MediaQuery(
// 노치 단말 — 상태 표시줄 47.
data: const MediaQueryData(
size: Size(540, 1200),
padding: EdgeInsets.only(top: 47),
),
child: HomeScreen(
repository: repo,
questRepository: const SampleQuestRepository(),
eclipse: const NoEclipseRepository(),
),
),
),
);
await tester.pumpAndSettle();
}
Finder get _hotArchive => find.byWidgetPredicate(
(w) => w is HomeSectionHeader && w.title == 'HOT ARCHIVE',
);
void main() {
setUp(() => SharedPreferences.setMockInitialValues({}));
group('HOT ARCHIVE — 모금 중 캠페인 0건(명세 §2.114-6)', () {
testWidgets('구획과 「더 보기」가 남아 캠페인 목록으로 간다', (tester) async {
await _pump(tester, const _NoCampaignRepo());
await tester.scrollUntilVisible(
_hotArchive,
220,
scrollable: find.byType(Scrollable).first,
);
await tester.ensureVisible(_hotArchive);
await tester.pumpAndSettle();
await tester.tap(
find.descendant(of: _hotArchive, matching: find.text('더 보기')),
);
await tester.pumpAndSettle();
expect(find.byType(CampaignListScreen), findsOneWidget);
});
testWidgets('카드 자리의 「새 캠페인」이 개설 화면을 연다', (tester) async {
await _pump(tester, const _NoCampaignRepo());
final create = find.text('새 캠페인');
await tester.scrollUntilVisible(
create,
220,
scrollable: find.byType(Scrollable).first,
);
await tester.tap(create);
await tester.pumpAndSettle();
expect(find.byType(CampaignCreateScreen), findsOneWidget);
});
});
testWidgets('스크롤하면 상태 표시줄 자리를 가린다(머리글 겹침 방지)', (tester) async {
await _pump(tester, const _NoCampaignRepo());
final scrim = find.byKey(const ValueKey('home-status-scrim'));
// 맨 위에서는 히어로 배경이 상태 표시줄 뒤까지 보여야 한다.
expect(scrim, findsNothing);
await tester.drag(find.byType(Scrollable).first, const Offset(0, -400));
await tester.pumpAndSettle();
expect(scrim, findsOneWidget);
expect(tester.getSize(scrim).height, 47);
await tester.drag(find.byType(Scrollable).first, const Offset(0, 2000));
await tester.pumpAndSettle();
expect(scrim, findsNothing);
});
group('FirestoreHomeRepository', () {
late FakeFirebaseFirestore db;
setUp(() async {
db = FakeFirebaseFirestore();
await db.doc('users/$_uid').set({'fandomId': 'fanuzu-demo'});
});
Future<void> brief(String id, String section, String status) =>
db.doc('briefings/$id').set({
'fandomId': 'fanuzu-demo',
'dayKey': kstDayKey(),
'status': status,
'section': section,
'rank': 1,
'headline': id,
'sourceName': '출처',
'sourceUrl': 'https://example.com/s',
'articleUrl': 'https://example.com/a',
'aiSummary': '요약',
'sourceAt': DateTime(2026, 10, 4),
});
test('브리핑 section 은 서버 값(News·SNS)만 그린다 — 시연 시드 소문자 결함', () async {
await brief('news', 'News', 'published');
await brief('sns', 'SNS', 'published');
await brief('draft', 'News', 'draft');
// 수정 전 시드(`tools/demo/fs.mjs`)가 쓰던 값 — 앱은 모르는 구획이라 버린다.
await brief('lower', 'news', 'published');
final home = await FirestoreHomeRepository(_Backend(db)).load();
expect(
home.briefings.map((b) => b.headline),
unorderedEquals(['news', 'sns']),
);
});
test('인기 K-INSIDE 는 미해결 먼저, 그 안에서 현상금 큰 순(명세 §2.114-7)', () async {
final at = DateTime(2026, 10, 4);
final rows = {
'new-0-answers': ('open', 100, at.add(const Duration(hours: 1))),
'solved-big': ('accepted', 900, at),
'open-500': ('open', 500, at),
'open-300': ('open', 300, at),
'open-200': ('open', 200, at),
};
for (final MapEntry(key: id, value: (status, bounty, createdAt))
in rows.entries) {
await db.doc('questions/$id').set({
'title': id,
'status': status,
'bountyPop': bounty,
'createdAt': createdAt,
});
}
final home = await FirestoreHomeRepository(_Backend(db)).load();
expect(home.insides.map((i) => i.title), [
'open-500',
'open-300',
'open-200',
]);
});
test('미해결이 3건보다 적으면 해결된 고액 질문으로 채운다', () {
expect(
popularInsides([
{'title': 'a', 'status': 'accepted'},
{'title': 'b', 'status': 'open'},
{'title': 'c', 'status': 'expired'},
]).map((q) => q['title']),
['b', 'a', 'c'],
);
});
});
}