HOME 을 Figma 「홈」 구성으로 — TODAY'S QUEST · 미니맵·확장 맵(프로토타입 v6.2) · 행성 정보 박스 · 인큐베이터 · 아티스트 소식 · HOT ARCHIVE · 인기 K-INSIDE
This commit is contained in:
@@ -250,8 +250,8 @@ void main() {
|
||||
'donorCount': 48, 'commentCount': 24, 'likeCount': 82,
|
||||
});
|
||||
final snapshot = await FirestoreHomeRepository(_backend(db)).load();
|
||||
expect(snapshot.campaign!.achievedPercent, 92);
|
||||
expect(snapshot.campaign!.supporters, 48);
|
||||
expect(snapshot.campaigns.first.achievedPercent, 92);
|
||||
expect(snapshot.campaigns.first.supporters, 48);
|
||||
});
|
||||
|
||||
test('차단한 개설자의 캠페인은 홈 대표 캠페인에서 건너뛴다', () async {
|
||||
@@ -266,7 +266,7 @@ void main() {
|
||||
});
|
||||
await db.doc('users/$_uid/blocks/troll').set({'nickname': '트롤'});
|
||||
final snapshot = await FirestoreHomeRepository(_backend(db)).load();
|
||||
expect(snapshot.campaign!.title, '다음');
|
||||
expect(snapshot.campaigns.first.title, '다음');
|
||||
});
|
||||
|
||||
test('모금 중이 아닌 캠페인은 홈에 올리지 않는다', () async {
|
||||
@@ -275,7 +275,7 @@ void main() {
|
||||
'status': 'review', 'title': '심사 중', 'goalPop': 100, 'raisedPop': 0,
|
||||
});
|
||||
final snapshot = await FirestoreHomeRepository(_backend(db)).load();
|
||||
expect(snapshot.campaign, isNull);
|
||||
expect(snapshot.campaigns, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -72,11 +72,16 @@ final shots = <Shot>[
|
||||
Shot('F1-01d', tab: '/home'),
|
||||
// 아래 구획(TODAY'S QUEST 등) 눈 대조용 긴 렌더.
|
||||
Shot('F1-01L', tab: '/home', h: 1400),
|
||||
// Figma 「홈」(`2097:16428`) 375×2904 전체 길이 — 사본 렌더와 나란히 대조.
|
||||
Shot('F1-01F', tab: '/home', h: 2904),
|
||||
Shot(
|
||||
'F1-01i',
|
||||
tab: '/home',
|
||||
after: (t) async {
|
||||
final hold = await t.startGesture(t.getCenter(find.text('기여')));
|
||||
// 떠 있는 ZAP 은 번개만 있다(라벨 없음) — 의미 라벨로 잡는다.
|
||||
final hold = await t.startGesture(
|
||||
t.getCenter(find.bySemanticsLabel(RegExp(r'^ZAP'))),
|
||||
);
|
||||
await t.pump(const Duration(seconds: 2));
|
||||
await hold.up();
|
||||
await _settle(t);
|
||||
|
||||
@@ -51,7 +51,8 @@ class QuietHome extends SampleHomeRepository {
|
||||
growth: growth ?? s.growth,
|
||||
quests: s.quests,
|
||||
briefings: s.briefings,
|
||||
campaign: s.campaign,
|
||||
campaigns: s.campaigns,
|
||||
nebulas: s.nebulas,
|
||||
insides: s.insides,
|
||||
items: s.items,
|
||||
);
|
||||
|
||||
@@ -226,6 +226,9 @@ void main() {
|
||||
testWidgets('두 조건을 갈라 보여 준다', (tester) async {
|
||||
await _pump(tester, const GrowthCompleteScreen());
|
||||
expect(find.text('활동을 통해 얻은 POP을 통해\n성운에 기여해 보세요!'), findsOneWidget);
|
||||
// 카드는 성장률 한 줄(사본 「성운 성장률 100%」) — 누르면 두 조건 상세.
|
||||
await tester.tap(find.textContaining('성운 성장률'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('POP 조건 충족 · 1,240 / 1,000'), findsOneWidget);
|
||||
expect(find.text('창립 멤버 조건 충족 · 12 / 10'), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -125,4 +125,39 @@ void main() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// HOME 「GALAXY 맵 확장」(`343:211127`) — 히어로 안 맵 영역 375×556.
|
||||
for (final (name, planets) in [
|
||||
('expanded_v2_app_mine1', protoPlanets),
|
||||
('expanded_v2_app_mine3', mineAt(3)),
|
||||
]) {
|
||||
testWidgets(name, (tester) async {
|
||||
final key = GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
key: key,
|
||||
child: SizedBox(
|
||||
width: 375,
|
||||
height: 556,
|
||||
child: GalaxyMiniMap.expanded(
|
||||
data: GalaxySnapshot(planets: planets, myPop: 0),
|
||||
onOpenPlanet: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.runAsync(() async {
|
||||
final boundary =
|
||||
key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final image = await boundary.toImage(pixelRatio: 3);
|
||||
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
File('$out/$name.png').writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import 'package:fancake/core/data/planet_visual_state.dart';
|
||||
import 'package:fancake/core/theme/app_theme.dart';
|
||||
import 'package:fancake/core/ui/planet_25d.dart';
|
||||
import 'package:fancake/features/galaxy/galaxy_data.dart';
|
||||
import 'package:fancake/features/galaxy/galaxy_screen.dart';
|
||||
import 'package:fancake/features/galaxy/galaxy_search_screen.dart';
|
||||
import 'package:fancake/features/planet/planet_detail_screen.dart';
|
||||
import 'package:fancake/features/galaxy/widgets/galaxy_mini_map.dart';
|
||||
import 'package:fancake/features/galaxy/widgets/galaxy_web_view.dart';
|
||||
import 'package:fancake/features/home/home_data.dart';
|
||||
import 'package:fancake/features/home/home_screen.dart';
|
||||
import 'package:fancake/features/home/widgets/home_hero.dart';
|
||||
@@ -329,11 +331,12 @@ void main() {
|
||||
expect(micros, lessThan(paintBudgetMicros));
|
||||
});
|
||||
|
||||
testWidgets('HOME 미니맵은 같은 저장소의 Galaxy로 진입한다', (tester) async {
|
||||
Future<StreamController<PlanetVisualState>> pumpHome(
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
tester.view.devicePixelRatio = 3;
|
||||
addTearDown(tester.view.reset);
|
||||
const galaxy = SampleGalaxyRepository();
|
||||
final states = StreamController<PlanetVisualState>.broadcast();
|
||||
addTearDown(states.close);
|
||||
await tester.pumpWidget(
|
||||
@@ -343,7 +346,7 @@ void main() {
|
||||
repository: const SampleHomeRepository(),
|
||||
questRepository: const SampleQuestRepository(),
|
||||
eclipse: const NoEclipseRepository(),
|
||||
galaxyRepository: galaxy,
|
||||
galaxyRepository: const SampleGalaxyRepository(),
|
||||
planetRepository: samplePlanetRepository,
|
||||
watchPlanet: (_) => states.stream,
|
||||
),
|
||||
@@ -351,13 +354,26 @@ void main() {
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
return states;
|
||||
}
|
||||
|
||||
GalaxyMiniMapPainter expandedPainter(WidgetTester tester) => tester
|
||||
.widgetList<CustomPaint>(
|
||||
find.descendant(
|
||||
of: find.byType(HomeGalaxyHero),
|
||||
matching: find.byType(CustomPaint),
|
||||
),
|
||||
)
|
||||
.map((w) => w.painter)
|
||||
.whereType<GalaxyMiniMapPainter>()
|
||||
.single;
|
||||
|
||||
testWidgets('HOME 미니맵 자리 — Figma 「홈」 히어로 우측 상단', (tester) async {
|
||||
final states = await pumpHome(tester);
|
||||
final mini = find.byType(GalaxyMiniMap);
|
||||
expect(mini, findsOneWidget);
|
||||
// C-105 약 140pt · C-113 자리 = Figma 「홈」(2097:16428) 히어로 우측 상단 · 오른쪽 여백 0.
|
||||
expect(
|
||||
tester.getSize(mini),
|
||||
const Size.square(HomeHero.miniMapSize),
|
||||
);
|
||||
// C-113 자리 = Figma 「홈」(2097:16428) 히어로 우측 상단 · 오른쪽 여백 0.
|
||||
expect(tester.getSize(mini), const Size.square(HomeHero.miniMapSize));
|
||||
expect(
|
||||
tester.getTopRight(mini).dx,
|
||||
closeTo(tester.getSize(find.byType(HomeScreen)).width, 1),
|
||||
@@ -372,15 +388,162 @@ void main() {
|
||||
1,
|
||||
),
|
||||
);
|
||||
await tester.tap(mini);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(
|
||||
tester.widget<GalaxyScreen>(find.byType(GalaxyScreen)).repository,
|
||||
same(galaxy),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
expect(states.hasListener, isFalse);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'미니맵을 누르면 히어로가 「GALAXY 맵 확장」(343:211127)으로 바뀌고, GALAXY · 뒤로가기로 돌아온다',
|
||||
(tester) async {
|
||||
await pumpHome(tester);
|
||||
await tester.tap(find.byType(GalaxyMiniMap));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.byType(HomeGalaxyHero), findsOneWidget);
|
||||
expect(find.byType(HomeHero), findsNothing);
|
||||
// 확장 맵 하나만 — 상단 미니맵은 사라진다. TODAY'S QUEST 등 아래 구획은 그대로.
|
||||
expect(find.byType(GalaxyMiniMap), findsOneWidget);
|
||||
expect(find.text('TODAY’S QUEST'), findsOneWidget);
|
||||
// 히어로 680 안 y124~680 = 맵(375×556), 아래 54 「GALAXY」 바.
|
||||
final map = find.byType(GalaxyMiniMap);
|
||||
expect(tester.getSize(map), const Size(360, 556)); // 시험 창 폭 360.
|
||||
expect(find.text('GALAXY'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('galaxy-bar-close')));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.byType(HomeGalaxyHero), findsNothing);
|
||||
expect(find.byType(HomeHero), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(GalaxyMiniMap));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.byType(HomeGalaxyHero), findsOneWidget);
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(find.byType(HomeGalaxyHero), findsNothing);
|
||||
expect(find.byType(HomeScreen), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('확장 맵 — 다른 행성을 누르면 그 행성이 화면 중심이 된다(2008:14486)', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: SizedBox(
|
||||
width: 375,
|
||||
height: 556,
|
||||
child: GalaxyMiniMap.expanded(
|
||||
data: _snap(_base),
|
||||
onOpenPlanet: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
final painter = tester
|
||||
.widgetList<CustomPaint>(find.byType(CustomPaint))
|
||||
.map((w) => w.painter)
|
||||
.whereType<GalaxyMiniMapPainter>()
|
||||
.single;
|
||||
expect(painter.focusId, 'a');
|
||||
// Figma `343:211127` 축척 — HUD 카드는 고정 144×72, 줌과 무관.
|
||||
expect(painter.hudRectOf('a')!.size, GalaxyMiniMapPainter.figmaHud);
|
||||
painter.zoomTo(2);
|
||||
await tester.pump();
|
||||
expect(painter.hudRectOf('a')!.size, GalaxyMiniMapPainter.figmaHud);
|
||||
painter.zoomTo(1);
|
||||
await tester.pump();
|
||||
final origin = tester.getTopLeft(find.byType(GalaxyMiniMap));
|
||||
final at = painter.screenOf('b')!;
|
||||
expect(painter.hudRectOf('b')!.contains(at), isFalse);
|
||||
await tester.tapAt(origin + at);
|
||||
await tester.pump();
|
||||
expect(painter.focusId, 'b');
|
||||
// 카메라가 따라가 고른 행성이 가운데로 온다.
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
expect(painter.screenOf('b')!.dx, closeTo(375 / 2, 2));
|
||||
});
|
||||
|
||||
testWidgets('확장 맵 — 끌어 회전 · 핀치 줌 · HUD 탭 상세 · 돋보기 검색', (tester) async {
|
||||
await pumpHome(tester);
|
||||
await tester.tap(find.byType(GalaxyMiniMap));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
final painter = expandedPainter(tester);
|
||||
final map = find.byType(GalaxyMiniMap);
|
||||
final origin = tester.getTopLeft(map);
|
||||
expect(painter.focusId, isNotNull);
|
||||
|
||||
// 끌기 — 회전(±180° 한계).
|
||||
await tester.dragFrom(tester.getCenter(map), const Offset(2000, 0));
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
expect(painter.rotation, closeTo(math.pi, 0.05));
|
||||
|
||||
// 핀치 — 두 손가락 벌리기.
|
||||
final c = tester.getCenter(map);
|
||||
final a = await tester.startGesture(c - const Offset(40, 0));
|
||||
final b = await tester.startGesture(c + const Offset(40, 0));
|
||||
await a.moveBy(const Offset(-40, 0));
|
||||
await b.moveBy(const Offset(40, 0));
|
||||
await tester.pump();
|
||||
await a.up();
|
||||
await b.up();
|
||||
await tester.pump();
|
||||
expect(painter.zoom, greaterThan(1.2));
|
||||
|
||||
// HUD 탭 → 행성 상세.
|
||||
await tester.pump(const Duration(milliseconds: 16));
|
||||
final focus = painter.focusId!;
|
||||
final hud = painter.hudRectOf(focus)!;
|
||||
expect(hud.isEmpty, isFalse);
|
||||
await tester.tapAt(origin + hud.center);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.byType(PlanetDetailScreen), findsOneWidget);
|
||||
Navigator.of(tester.element(find.byType(PlanetDetailScreen))).pop();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
// 돋보기 → 갤럭시 검색.
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.byType(GalaxySearchScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
test('WebView 로 넘기는 데이터 — FPI 상위 10 + 순위 밖 내 행성 · 실제 FPI · 색은 RGB 정수', () {
|
||||
final planets = [
|
||||
for (var i = 1; i <= 12; i++)
|
||||
PlanetNode(
|
||||
id: 'p$i',
|
||||
name: 'P$i',
|
||||
rank: i,
|
||||
fpi: 100000 - i,
|
||||
members: 1,
|
||||
online: i,
|
||||
isMine: i == 12,
|
||||
),
|
||||
];
|
||||
final list = GalaxyWebView.payload(_snap(planets));
|
||||
expect(list, hasLength(11));
|
||||
expect(list.first['fpi'], 99999);
|
||||
expect(list.last['id'], 'p12');
|
||||
expect(list.last['mine'], isTrue);
|
||||
expect(list.every((p) => (p['color']! as int) <= 0xFFFFFF), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('미니맵 → 확장은 같은 지도 위젯 상태를 옮긴다(WebView 재생성 없음)', (tester) async {
|
||||
await pumpHome(tester);
|
||||
final before = tester.state(find.byType(GalaxyWebView));
|
||||
expect(tester.widget<GalaxyWebView>(find.byType(GalaxyWebView)).expanded, isFalse);
|
||||
await tester.tap(find.byType(GalaxyMiniMap));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 300));
|
||||
expect(tester.state(find.byType(GalaxyWebView)), same(before));
|
||||
expect(tester.widget<GalaxyWebView>(find.byType(GalaxyWebView)).expanded, isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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/core/ui/section_header.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';
|
||||
@@ -49,7 +49,7 @@ class _NoCampaignRepo extends SampleHomeRepository {
|
||||
growth: base.growth,
|
||||
quests: base.quests,
|
||||
briefings: base.briefings,
|
||||
campaign: null,
|
||||
campaigns: const [],
|
||||
insides: base.insides,
|
||||
);
|
||||
}
|
||||
@@ -80,7 +80,7 @@ Future<void> _pump(WidgetTester tester, HomeRepository repo) async {
|
||||
}
|
||||
|
||||
Finder get _hotArchive => find.byWidgetPredicate(
|
||||
(w) => w is SectionHeader && w.title == 'HOT ARCHIVE',
|
||||
(w) => w is HomeSectionHeader && w.title == 'HOT ARCHIVE',
|
||||
);
|
||||
|
||||
void main() {
|
||||
@@ -94,6 +94,8 @@ void main() {
|
||||
220,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
await tester.ensureVisible(_hotArchive);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(
|
||||
find.descendant(of: _hotArchive, matching: find.text('더 보기')),
|
||||
);
|
||||
|
||||
+122
-32
@@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:fancake/core/ui/section_header.dart';
|
||||
import 'package:fancake/features/home/widgets/home_sections.dart';
|
||||
import 'package:fancake/features/quest/quest_list_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -7,8 +7,12 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fancake/core/theme/app_theme.dart';
|
||||
import 'package:fancake/features/home/home_data.dart';
|
||||
import 'package:fancake/features/home/home_screen.dart';
|
||||
import 'package:fancake/features/home/home_repository_firestore.dart' show adminQuestsLatest, nebulaPicks;
|
||||
import 'package:fancake/features/quest/quest_data.dart';
|
||||
import 'package:fancake/features/home/widgets/briefing_list.dart';
|
||||
import 'package:fancake/features/home/widgets/quest_row.dart';
|
||||
import 'package:fancake/features/inside/inside_detail_screen.dart';
|
||||
import 'package:fancake/features/quest/widgets/quest_card_tile.dart' show QuestIconBox;
|
||||
|
||||
import 'eclipse_stubs.dart';
|
||||
|
||||
@@ -18,6 +22,36 @@ class _LiveGrowthRepo extends SampleHomeRepository {
|
||||
Stream<PlanetGrowth> watchGrowth(String fandomId) => changes.stream;
|
||||
}
|
||||
|
||||
class _LiveWalletRepo extends SampleHomeRepository {
|
||||
final wallet = StreamController<int>.broadcast();
|
||||
@override
|
||||
Stream<int> watchPopBalance() => wallet.stream;
|
||||
}
|
||||
|
||||
/// 퀘스트 4건 — 3줄까지만 그리는지 본다.
|
||||
class _QuestRepo extends SampleHomeRepository {
|
||||
const _QuestRepo();
|
||||
@override
|
||||
Future<HomeSnapshot> load() async {
|
||||
final base = await const SampleHomeRepository().load();
|
||||
return HomeSnapshot(
|
||||
popBalance: base.popBalance,
|
||||
fandomId: base.fandomId,
|
||||
liveTicker: base.liveTicker,
|
||||
growth: base.growth,
|
||||
quests: [
|
||||
...base.quests,
|
||||
const QuestSummary(title: '좋아요', rewardPop: 5, state: QuestState.done),
|
||||
const QuestSummary(title: '네 번째', rewardPop: 5, state: QuestState.inProgress),
|
||||
],
|
||||
briefings: base.briefings,
|
||||
campaigns: base.campaigns,
|
||||
nebulas: base.nebulas,
|
||||
insides: base.insides,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 값을 못 가져오는 경우를 재현한다.
|
||||
class _FailingRepo extends SampleHomeRepository {
|
||||
@override
|
||||
@@ -61,7 +95,8 @@ class _BriefingRepo extends SampleHomeRepository {
|
||||
growth: base.growth,
|
||||
quests: base.quests,
|
||||
briefings: briefings,
|
||||
campaign: base.campaign,
|
||||
campaigns: base.campaigns,
|
||||
nebulas: base.nebulas,
|
||||
insides: base.insides,
|
||||
);
|
||||
}
|
||||
@@ -98,20 +133,36 @@ void main() {
|
||||
requiredPop: 10000, currentSfpi: 750, requiredSfpi: 1000,
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('POP 조건 충족 · 10,000 / 10,000'), findsOneWidget);
|
||||
expect(find.text('sFPI 조건 미충족 · 750 / 1,000'), findsOneWidget);
|
||||
// 카드는 병목 축(sFPI 75%)을 보인다.
|
||||
expect(find.text('행성 성장률 75%'), findsOneWidget);
|
||||
expect(find.text('750 / 1,000'), findsOneWidget);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump();
|
||||
expect(repo.changes.hasListener, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('헤더 POP 칩은 지갑 문서 변동을 따라간다(다른 화면의 예치·보상)', (tester) async {
|
||||
final repo = _LiveWalletRepo();
|
||||
addTearDown(repo.wallet.close);
|
||||
await _pump(tester, repo: repo);
|
||||
expect(find.text('12,540'), findsOneWidget);
|
||||
expect(repo.wallet.hasListener, isTrue);
|
||||
repo.wallet.add(12160);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('12,160'), findsOneWidget);
|
||||
repo.wallet.add(12140);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('12,140'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Figma 홈의 구획 제목이 순서대로 나온다', (tester) async {
|
||||
await _pump(tester);
|
||||
// 화면 대조 원장 F1 · Figma `252:159401` 이하의 구획명 그대로여야 한다.
|
||||
// Figma 「홈」 `2097:16428` `Frame 4241` 구획명 그대로여야 한다.
|
||||
// 기본 뷰포트(800×600)보다 화면이 길어 아래쪽은 스크롤해야 잡힌다.
|
||||
for (final title in [
|
||||
'TODAY’S QUEST',
|
||||
'AI Daily Briefing',
|
||||
'팬덤행성 인큐베이터',
|
||||
'아티스트 소식',
|
||||
'HOT ARCHIVE',
|
||||
'인기 K-INSIDE',
|
||||
]) {
|
||||
@@ -124,25 +175,71 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('POP 진행률과 두 성장 조건을 구분해서 표시한다', (tester) async {
|
||||
testWidgets('성장 카드는 Figma 배치 — 성장률 · 수치 한 쌍, 말풍선을 누르면 두 조건 상세', (tester) async {
|
||||
await _pump(tester);
|
||||
expect(find.text('67%'), findsOneWidget);
|
||||
expect(find.text('POP 진행률'), findsOneWidget);
|
||||
// 표본은 sFPI 값이 없어 POP 축(6,270 / 10,000)이 기준이다.
|
||||
expect(find.text('행성 성장률 63%'), findsOneWidget);
|
||||
expect(find.text('6,270 / 10,000'), findsOneWidget);
|
||||
// 카드에 두 조건 줄을 늘어놓지 않는다.
|
||||
expect(find.text('POP 조건 미충족 · 6,270 / 10,000'), findsNothing);
|
||||
// 말풍선이 성장률 글자를 가리지 않는다.
|
||||
final rate = tester.getRect(find.text('행성 성장률 63%'));
|
||||
final bubble = tester.getRect(find.text('다음 진화까지 얼마 안남았어요!'));
|
||||
expect(bubble.left, greaterThan(rate.right));
|
||||
await tester.tap(find.text('다음 진화까지 얼마 안남았어요!'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('POP 조건 미충족 · 6,270 / 10,000'), findsOneWidget);
|
||||
expect(find.text('sFPI 조건 확인 중 · — / —'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('TODAY’S QUEST 는 관리자 퀘스트만 최신순 — 사용자 생성 퀘스트는 빠진다(2097:16397)', () {
|
||||
final docs = [
|
||||
{'id': 'old', 'createdAt': DateTime(2026, 9, 1)},
|
||||
{'id': 'user', 'authorUid': 'u1', 'createdAt': DateTime(2026, 10, 4)},
|
||||
{'id': 'new', 'createdAt': DateTime(2026, 10, 1)},
|
||||
];
|
||||
expect(adminQuestsLatest(docs, (d) => d).map((d) => d['id']), ['new', 'old']);
|
||||
});
|
||||
|
||||
test('인큐베이터는 기여율 높은 순 최대 6개(명세 §2.114-4)', () {
|
||||
final picks = nebulaPicks([
|
||||
for (var i = 0; i < 8; i++) ('n$i', {'name': 'N$i', 'growthPop': i * 100}),
|
||||
], 1000);
|
||||
expect(picks, hasLength(6));
|
||||
expect(picks.first.id, 'n7');
|
||||
expect(picks.first.percent, 70);
|
||||
});
|
||||
|
||||
test('성장률 기준은 두 조건 중 진행률이 낮은 쪽이다', () {
|
||||
const g = PlanetGrowth(
|
||||
stage: 2, fandomName: 'X', contributorName: '', contributedPop: 0,
|
||||
growthPercent: 90, currentPop: 9000, requiredPop: 10000,
|
||||
currentSfpi: 300, requiredSfpi: 1000,
|
||||
);
|
||||
expect(g.bottleneck!.ratio, closeTo(0.3, 1e-9));
|
||||
expect(g.bottleneck!.current, 300);
|
||||
expect(g.growthBubble, '성장 조건 확인');
|
||||
});
|
||||
|
||||
testWidgets('POP 잔액은 천 단위로 끊어 띄운다', (tester) async {
|
||||
await _pump(tester);
|
||||
expect(find.text('12,540'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('퀘스트 카드는 보상과 진행 상태를 보여 준다', (tester) async {
|
||||
await _pump(tester);
|
||||
// 시안(가로 카드)은 제목 옆에 「+보상」, 아래에 상태 한 줄.
|
||||
expect(find.text('+10'), findsOneWidget);
|
||||
expect(find.text('출석 완료'), findsOneWidget);
|
||||
expect(find.text('0/1,500 걸음'), findsOneWidget);
|
||||
testWidgets('TODAY’S QUEST 는 Figma 「홈」 대로 세로 줄이다 — 높이 90 · 아이콘 상자 50 · 최대 3줄', (tester) async {
|
||||
await _pump(tester, repo: const _QuestRepo());
|
||||
final rows = find.byType(QuestRow);
|
||||
expect(rows, findsNWidgets(3));
|
||||
final first = tester.getRect(rows.at(0));
|
||||
final second = tester.getRect(rows.at(1));
|
||||
expect(first.height, 90);
|
||||
expect(second.top, first.bottom); // 간격 없이 쌓는다(`2097:16428`).
|
||||
expect(second.left, first.left);
|
||||
expect(tester.getSize(find.byType(QuestIconBox).first), const Size(50, 50));
|
||||
// 보조 줄은 카테고리, 보상 숫자 · 진행 상태는 그리지 않는다(사본 「홈」).
|
||||
expect(find.text('오늘의 출석체크'), findsOneWidget);
|
||||
expect(find.text('+10'), findsNothing);
|
||||
expect(find.text('네 번째'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('현상금 표기에 천 단위 구분이 들어간다', (tester) async {
|
||||
@@ -155,23 +252,16 @@ void main() {
|
||||
expect(find.text('500 P'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('아직 없는 화면으로 가려 하면 준비 중이라고 알린다', (tester) async {
|
||||
testWidgets('인기 K-INSIDE 카드를 누르면 인사이드 상세로 간다', (tester) async {
|
||||
await _pump(tester);
|
||||
// 첫 「더 보기」는 퀘스트라 실제로 이동한다. 아직 없는 구간까지 굴려서 누른다.
|
||||
// 브리핑 헤더는 「공유」다(`266:90529`). 아직 없는 목록인 인기 K-INSIDE 로 본다.
|
||||
final briefing = find.byWidgetPredicate(
|
||||
(w) => w is SectionHeader && w.title == '인기 K-INSIDE',
|
||||
);
|
||||
await tester.scrollUntilVisible(briefing, 220,
|
||||
final card = find.text('화양연화 세계관 해석해주실 분?');
|
||||
await tester.scrollUntilVisible(card, 220,
|
||||
scrollable: find.byType(Scrollable).first);
|
||||
await tester.ensureVisible(briefing);
|
||||
await tester.ensureVisible(card);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.descendant(of: briefing, matching: find.text('더 보기')));
|
||||
await tester.pump();
|
||||
// 조용히 무시하면 검수 때 고장으로 보인다.
|
||||
expect(find.textContaining('화면은 준비 중입니다.'), findsOneWidget);
|
||||
// 내부 일정(구현 확인물 · 날짜)은 사용자 화면에 내보내지 않는다.
|
||||
expect(find.textContaining('구현 확인물'), findsNothing);
|
||||
await tester.tap(card);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(InsideDetailScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('오늘의 퀘스트에서 퀘스트 목록으로 넘어간다', (tester) async {
|
||||
@@ -202,7 +292,7 @@ void main() {
|
||||
/// *문구도 노출하지 않음)*. 서버가 당일 게시분을 0 건으로 주는 상황이다.
|
||||
testWidgets('브리핑이 0 건이면 구획 제목까지 숨긴다', (tester) async {
|
||||
await _pump(tester, repo: const _BriefingRepo([]));
|
||||
expect(find.text('AI Daily Briefing'), findsNothing);
|
||||
expect(find.text('아티스트 소식'), findsNothing);
|
||||
// 빈 상태 문구도 두지 않는다 — 아래 구획은 그대로 있어야 한다.
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('HOT ARCHIVE'),
|
||||
@@ -215,7 +305,7 @@ void main() {
|
||||
testWidgets('브리핑 헤더의 「공유」는 브리핑 공유 카드를 연다(F8 · 별첨 1 ③-5)', (tester) async {
|
||||
await _pump(tester, repo: _BriefingRepo([_brief('공유할 한 줄')]));
|
||||
final header = find.byWidgetPredicate(
|
||||
(w) => w is SectionHeader && w.title == 'AI Daily Briefing',
|
||||
(w) => w is HomeSectionHeader && w.title == '아티스트 소식',
|
||||
);
|
||||
await tester.scrollUntilVisible(find.text('공유할 한 줄'), 220,
|
||||
scrollable: find.byType(Scrollable).first);
|
||||
@@ -234,7 +324,7 @@ void main() {
|
||||
220,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
expect(find.text('AI Daily Briefing'), findsOneWidget);
|
||||
expect(find.text('아티스트 소식'), findsOneWidget);
|
||||
expect(find.text('오늘 게시된 한 줄'), findsOneWidget);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user