586 lines
24 KiB
Dart
586 lines
24 KiB
Dart
import 'package:fancake/features/eclipse/eclipse_data.dart';
|
|
import 'package:fancake/core/ui/relative_time.dart';
|
|
import 'package:fancake/features/galaxy/galaxy_data.dart';
|
|
import 'package:fancake/features/planet/fpi_ranking_screen.dart';
|
|
import 'package:fancake/features/planet/planet_data.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:fancake/core/theme/app_theme.dart';
|
|
import 'package:fancake/features/planet/lounge_post_screens.dart';
|
|
import 'package:fancake/features/planet/moderation.dart';
|
|
import 'package:fancake/features/planet/planet_detail_screen.dart';
|
|
|
|
import 'eclipse_stubs.dart';
|
|
|
|
Future<void> _pump(WidgetTester tester, Widget screen) async {
|
|
await tester.pumpWidget(MaterialApp(theme: buildAppTheme(), home: screen));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
/// 세로가 긴 화면을 한 번에 담는다.
|
|
///
|
|
/// TabBarView 안에는 스크롤 가능한 것이 여럿이라 `scrollUntilVisible` 이 엉뚱한
|
|
/// 쪽을 잡는다. 실기기에 가까운 세로 길이를 주는 편이 확실하다.
|
|
Future<void> _pumpTall(
|
|
WidgetTester tester,
|
|
Widget screen, {
|
|
double height = 2400,
|
|
}) async {
|
|
tester.view.physicalSize = Size(1080, height);
|
|
tester.view.devicePixelRatio = 2;
|
|
addTearDown(tester.view.reset);
|
|
await _pump(tester, screen);
|
|
}
|
|
|
|
/// 서버 산출 「내 기여 상위 %」가 아직 없는 상태의 저장소.
|
|
class _NoTopPercentRepository extends SamplePlanetRepository {
|
|
@override
|
|
Future<void> join() async {}
|
|
|
|
@override
|
|
Future<PlanetHistoryPage> history({String? cursor, bool personal = false}) => _sample.history(cursor: cursor);
|
|
@override
|
|
Future<HeroDropReceipt> claimHeroDrop(String dropId) => _sample.claimHeroDrop(dropId);
|
|
const _NoTopPercentRepository();
|
|
|
|
static const _sample = SamplePlanetRepository();
|
|
|
|
@override
|
|
Future<PlanetDetail> load({required bool nebula}) async {
|
|
final detail = await _sample.load(nebula: nebula);
|
|
return PlanetDetail(
|
|
fandomId: detail.fandomId,
|
|
name: detail.name,
|
|
isNebula: detail.isNebula,
|
|
totalFpi: detail.totalFpi,
|
|
members: detail.members,
|
|
posts: detail.posts,
|
|
scores: [
|
|
for (final s in detail.scores)
|
|
FpiScore(
|
|
metric: s.metric,
|
|
value: s.value,
|
|
// 집계 모집단 밖(서버 fpiRanks 없음)이면 순위가 0 으로 온다.
|
|
rank: 0,
|
|
trend: s.trend,
|
|
myTopPercent: null,
|
|
),
|
|
],
|
|
mockNote: '「내 기여 상위 %」 집계 함수가 아직 없습니다',
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<LoungePost>> morePosts({required int offset, int limit = 5}) =>
|
|
_sample.morePosts(offset: offset, limit: limit);
|
|
|
|
@override
|
|
Future<FpiRanking> planetRanking({required FpiMetric metric, int limit = 30}) =>
|
|
_sample.planetRanking(metric: metric, limit: limit);
|
|
|
|
@override
|
|
Future<FpiRanking> userRanking({
|
|
required FpiMetric metric,
|
|
required String fandomId,
|
|
int limit = 30,
|
|
}) => _sample.userRanking(metric: metric, fandomId: fandomId, limit: limit);
|
|
|
|
@override
|
|
Future<FpiRanking> countryRanking({required FpiMetric metric, int limit = 30}) =>
|
|
_sample.countryRanking(metric: metric, limit: limit);
|
|
}
|
|
|
|
/// 가입 전 상태로 시작하고, [join] 이 불리면 가입중으로 바뀌는 표본.
|
|
class _Joinable extends SamplePlanetRepository {
|
|
_Joinable();
|
|
int joins = 0;
|
|
|
|
@override
|
|
Future<void> join() async => joins++;
|
|
|
|
@override
|
|
Future<PlanetDetail> load({required bool nebula}) async {
|
|
final d = await super.load(nebula: nebula);
|
|
return PlanetDetail(
|
|
fandomId: d.fandomId, name: d.name, stage: d.stage, isNebula: d.isNebula,
|
|
totalFpi: d.totalFpi, scores: d.scores, members: d.members, posts: d.posts,
|
|
joined: joins > 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
_fpiRankingTests();
|
|
testWidgets('가입하지 않은 행성은 「가입하기」를 눌러 fn-fandom-join 으로 가입한다', (tester) async {
|
|
final repo = _Joinable();
|
|
await _pumpTall(tester, PlanetDetailScreen(repository: repo, eclipse: const NoEclipseRepository()));
|
|
expect(find.text('가입하기'), findsOneWidget);
|
|
await tester.tap(find.text('가입하기'));
|
|
await tester.pumpAndSettle();
|
|
expect(repo.joins, 1);
|
|
expect(find.text('가입중'), findsOneWidget);
|
|
// 가입중 알약은 다시 눌러도 호출하지 않는다.
|
|
await tester.tap(find.text('가입중'));
|
|
await tester.pumpAndSettle();
|
|
expect(repo.joins, 1);
|
|
});
|
|
group('행성 상세 · FPI (F3 · 343:213428)', () {
|
|
testWidgets('TOTAL FPI 값과 갱신 주기를 띄운다', (tester) async {
|
|
await _pumpTall(tester, const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
));
|
|
expect(find.text('TOTAL FPI : 284,310'), findsOneWidget);
|
|
// 원장 B-15 검증 기준 — 화면에 마지막 갱신 시각 노출(서버 시각이 없으면 --:--).
|
|
expect(find.text('마지막 갱신 --:--'), findsOneWidget);
|
|
expect(find.textContaining('30초'), findsNothing);
|
|
// 개설자 이름은 서버에 없으면 그리지 않는다 — 시안 표본 이름을 박지 않는다.
|
|
expect(find.text('BTS_Maker'), findsNothing);
|
|
});
|
|
|
|
testWidgets('5지표를 답변서 확정 표기로 띄운다', (tester) async {
|
|
// 시안(373:441117)은 1243 높이의 스크롤 화면이라 지표 다섯을 한 창에 담으려면
|
|
// 실기기보다 긴 창이 필요하다. 스크롤하면 위 카드가 해제되어 다섯을 동시에 못 본다.
|
|
await _pumpTall(tester, const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
), height: 4200);
|
|
// 답변서 v1.1 Q3 이 정본이다. Figma 의 BMP·STEPS·REFERAL 이 아니다.
|
|
for (final label in [
|
|
'BPM',
|
|
'HEALTH',
|
|
'REFERRAL',
|
|
'COMMUNITY',
|
|
'CONTRIBUTION',
|
|
]) {
|
|
expect(find.text(label), findsWidgets, reason: '지표 「$label」 누락');
|
|
}
|
|
});
|
|
|
|
testWidgets('지표마다 전체 순위와 내 기여 상위를 보여 준다', (tester) async {
|
|
await _pumpTall(tester, const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
), height: 4200);
|
|
expect(find.text('전체 1위'), findsOneWidget);
|
|
expect(find.text('상위 17%'), findsWidgets);
|
|
});
|
|
|
|
testWidgets('집계 값이 없으면 숫자를 짓지 않고 내부 사유도 띄우지 않는다 — 대조표 3-5', (tester) async {
|
|
await _pumpTall(
|
|
tester,
|
|
const PlanetDetailScreen(
|
|
repository: _NoTopPercentRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
),
|
|
height: 4200,
|
|
);
|
|
// 없는 값을 100%(최하위)로 지어내지 않는다.
|
|
expect(find.text('상위 100%'), findsNothing);
|
|
expect(find.text('—'), findsWidgets);
|
|
// 개발용 사유(MOCK · 집계 함수)는 사용자 화면에 내보내지 않는다(10/4 실기기 3-1).
|
|
expect(find.textContaining('MOCK'), findsNothing);
|
|
expect(find.textContaining('집계 함수'), findsNothing);
|
|
// 순위가 없으면 「전체 0위」 대신 「순위 집계 전」.
|
|
expect(find.text('전체 0위'), findsNothing);
|
|
expect(find.text('순위 집계 전'), findsWidgets);
|
|
});
|
|
|
|
testWidgets('이클립스가 발동 중이면 침식·방어 진행을 띄운다', (tester) async {
|
|
// 스펙 B — 「집계 문서를 갤럭시·행성 상세 HUD 에서 실시간으로 표시한다」.
|
|
// 침식은 TOTAL FPI 를 깎는 경로이므로(원장 B-55) 그 카드 위에 온다.
|
|
await _pumpTall(tester, PlanetDetailScreen(
|
|
repository: const SamplePlanetRepository(),
|
|
eclipse: SampleEclipseRepository(),
|
|
));
|
|
expect(find.textContaining('이클립스 발동'), findsOneWidget);
|
|
expect(find.textContaining('침식 42%'), findsOneWidget);
|
|
// **POP 단위가 아니다** — 방어는 POP 을 소모하지 않는 공동 Shake 참여다
|
|
// (Firestore 설계서 v1.1 §9-#5).
|
|
expect(find.textContaining('유효 참여 5,800 / 10,000명'), findsOneWidget);
|
|
expect(find.textContaining('POP'), findsNothing);
|
|
});
|
|
|
|
testWidgets('이클립스가 없으면 침식 배너 자리가 비지 않는다', (tester) async {
|
|
await _pumpTall(tester, const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
));
|
|
expect(find.textContaining('이클립스 발동'), findsNothing);
|
|
// 배너가 없어도 본문은 그대로다 — 빈 자리가 남지 않는다.
|
|
expect(find.textContaining('TOTAL FPI'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('팬덤 라운지 (F3 · 251:51073)', () {
|
|
testWidgets('최초 5개를 최신순으로 띄운다', (tester) async {
|
|
await _pumpTall(tester, const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
));
|
|
await tester.tap(find.text('팬덤 라운지'));
|
|
await tester.pumpAndSettle();
|
|
// 명세 — 최초 5개, 스크롤 시 5개씩 호출.
|
|
expect(find.textContaining('오늘 공방 다녀왔어요'), findsOneWidget);
|
|
expect(find.byType(FloatingActionButton), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('스크롤하면 히어로가 접히고 탭은 위에 남는다 — 발주사 지적(글이 보이게)', (tester) async {
|
|
tester.view.physicalSize = const Size(750, 1624);
|
|
tester.view.devicePixelRatio = 2;
|
|
addTearDown(tester.view.reset);
|
|
await _pump(
|
|
tester,
|
|
const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
),
|
|
);
|
|
await tester.tap(find.text('팬덤 라운지'));
|
|
await tester.pumpAndSettle();
|
|
final tabTop = tester.getTopLeft(find.text('팬덤 라운지')).dy;
|
|
await tester.drag(
|
|
find.textContaining('오늘 공방 다녀왔어요'),
|
|
const Offset(0, -500),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
// 탭 줄이 앱 바 바로 아래로 올라와 고정되고, 글이 화면 위쪽까지 올라온다.
|
|
expect(tester.getTopLeft(find.text('팬덤 라운지')).dy, lessThan(tabTop - 300));
|
|
expect(find.text('팬덤 라운지').hitTestable(), findsOneWidget);
|
|
expect(find.textContaining('이번 앨범 최애 트랙').hitTestable(), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('이 회원의 글 모두 숨기기 — 확인하면 그 작성자 글이 목록에서 빠진다', (tester) async {
|
|
await _pumpTall(
|
|
tester,
|
|
const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
),
|
|
);
|
|
await tester.tap(find.text('팬덤 라운지'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('아미보이'), findsOneWidget);
|
|
await tester.tap(
|
|
find.image(const AssetImage('assets/figma/icon-post-more.png')).first,
|
|
);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('이 회원의 글 모두 숨기기'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('확인'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('아미보이'), findsNothing);
|
|
});
|
|
|
|
testWidgets('글 쓰기는 본문이 있어야 완료된다', (tester) async {
|
|
// 시안 244:21746 · 설계서 §2.6 posts — 제목 칸 없이 본문 한 칸.
|
|
await _pump(tester, const LoungeComposeScreen());
|
|
final register = find.widgetWithText(TextButton, '완료');
|
|
expect(tester.widget<TextButton>(register).onPressed, isNull);
|
|
|
|
await tester.enterText(find.byType(TextField), ' ');
|
|
await tester.pumpAndSettle();
|
|
// 공백만으로는 아직 안 된다.
|
|
expect(tester.widget<TextButton>(register).onPressed, isNull);
|
|
|
|
await tester.enterText(find.byType(TextField), '본문입니다');
|
|
await tester.pumpAndSettle();
|
|
expect(tester.widget<TextButton>(register).onPressed, isNotNull);
|
|
});
|
|
});
|
|
|
|
group('신고 · 차단 (F3 · 252:162674 · 252:163306)', () {
|
|
testWidgets('사유를 고르기 전에는 신고할 수 없다', (tester) async {
|
|
await _pump(
|
|
tester,
|
|
const ReportScreen(
|
|
target: ModerationTarget(authorName: '아미보이', title: '테스트 글'),
|
|
),
|
|
);
|
|
// 신고 정보 영역 — 작성자 닉네임 · 게시물 제목
|
|
expect(find.text('아미보이'), findsOneWidget);
|
|
expect(find.text('테스트 글'), findsOneWidget);
|
|
final btn = tester.widget<TextButton>(find.widgetWithText(TextButton, '신고'));
|
|
expect(btn.onPressed, isNull);
|
|
});
|
|
|
|
testWidgets('사유는 단수 선택이고 고르면 확인을 되묻는다', (tester) async {
|
|
await _pump(
|
|
tester,
|
|
const ReportScreen(
|
|
target: ModerationTarget(authorName: '아미보이', title: '테스트 글'),
|
|
),
|
|
);
|
|
await tester.tap(find.text('부적절한 콘텐츠'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.widgetWithText(TextButton, '신고'));
|
|
await tester.pumpAndSettle();
|
|
// 화면 중앙 오버레이 — 취소 / 확인
|
|
expect(find.textContaining('신고할까요?'), findsOneWidget);
|
|
expect(find.text('취소'), findsOneWidget);
|
|
expect(find.text('확인'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('신고 사유는 어드민 등록 항목을 받는다', (tester) async {
|
|
await _pump(
|
|
tester,
|
|
const ReportScreen(
|
|
target: ModerationTarget(authorName: 'x', title: 'y'),
|
|
reasons: ['운영자가 정한 사유'],
|
|
),
|
|
);
|
|
expect(find.text('운영자가 정한 사유'), findsOneWidget);
|
|
expect(find.text('부적절한 콘텐츠'), findsNothing);
|
|
});
|
|
});
|
|
|
|
group('성운 상세 (F3 · 373:442708)', () {
|
|
testWidgets('성운도 같은 화면으로 열린다', (tester) async {
|
|
await _pump(tester, const PlanetDetailScreen(
|
|
nebula: true,
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
));
|
|
expect(find.text('New_BTS'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
testWidgets('상대 시각 표기', (tester) async {
|
|
final now = DateTime(2026, 8, 31, 15);
|
|
expect(relativeTime(now.subtract(const Duration(seconds: 20)), now: now), '방금 전');
|
|
expect(relativeTime(now.subtract(const Duration(minutes: 10)), now: now), '10분 전');
|
|
expect(relativeTime(now.subtract(const Duration(hours: 3)), now: now), '3시간 전');
|
|
expect(relativeTime(now.subtract(const Duration(days: 2)), now: now), '2일 전');
|
|
});
|
|
|
|
testWidgets('F3 화면 문구에 내부 경로가 새지 않는다', (tester) async {
|
|
await _pump(tester, const PlanetDetailScreen(
|
|
repository: SamplePlanetRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
));
|
|
final texts = tester
|
|
.widgetList<Text>(find.byType(Text))
|
|
.map((w) => w.data ?? '')
|
|
.join('\n');
|
|
for (final leak in ['lib/', '.dart', 'Exception', 'wireframe/']) {
|
|
expect(texts.contains(leak), isFalse, reason: '「$leak」 노출');
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/// 지표별 랭킹 2단계 — F3 · Figma `546:14589`(행성) → `546:14646`(사용자).
|
|
///
|
|
/// 발주사 검토회신서 4절 회귀 가드다. 지적은 두 가지였다 —
|
|
/// ① 지표 카드와 Ranking 사이 수치·순위 불일치 ② 지표를 바꿔도 같은 Ranking.
|
|
/// 둘 다 여기서 깨지면 바로 보이게 고정한다.
|
|
void _fpiRankingTests() {
|
|
const repo = SamplePlanetRepository();
|
|
|
|
group('표본은 행성별로 갈린다 — 어느 HUD 를 눌러도 같은 상세가 열리면 안 된다', () {
|
|
// 갤럭시 HUD 는 남의 행성도 연다. 표본이 내 행성 하나로 고정돼 있으면
|
|
// 여섯 행성이 전부 BTS 상세로 열려 발주사 확인에서 바로 드러난다.
|
|
test('행성마다 이름·TOTAL FPI·멤버 수가 다르다', () async {
|
|
final details = <PlanetDetail>[
|
|
for (final id in [
|
|
'bts',
|
|
'newjeans',
|
|
'seventeen',
|
|
'ive',
|
|
'aespa',
|
|
'lesserafim',
|
|
])
|
|
await SamplePlanetRepository(fandomId: id).load(nebula: false),
|
|
];
|
|
|
|
expect(details.map((d) => d.fandomId).toSet(), hasLength(6));
|
|
expect(details.map((d) => d.name).toSet(), hasLength(6));
|
|
expect(details.map((d) => d.totalFpi).toSet(), hasLength(6));
|
|
expect(details.map((d) => d.members).toSet(), hasLength(6));
|
|
});
|
|
|
|
test('상세의 TOTAL FPI 와 멤버 수가 갤럭시 맵 표본과 같다', () async {
|
|
// 같은 행성인데 HUD 와 상세가 다른 수치를 띄우면 그것부터 지적받는다.
|
|
final snapshot = await const SampleGalaxyRepository().load();
|
|
for (final node in snapshot.planets) {
|
|
final detail =
|
|
await SamplePlanetRepository(fandomId: node.id).load(nebula: false);
|
|
expect(detail.totalFpi, node.fpi, reason: '${node.id} TOTAL FPI 불일치');
|
|
expect(detail.members, node.members, reason: '${node.id} 멤버 수 불일치');
|
|
expect(detail.name, node.name, reason: '${node.id} 이름 불일치');
|
|
}
|
|
});
|
|
|
|
test('전체 순위는 그 행성의 순위다 — 내 행성 순위를 돌려주지 않는다', () async {
|
|
final detail =
|
|
await SamplePlanetRepository(fandomId: 'lesserafim').load(nebula: false);
|
|
for (final score in detail.scores) {
|
|
final rows = (await const SamplePlanetRepository()
|
|
.planetRanking(metric: score.metric))
|
|
.rows;
|
|
final row = rows.firstWhere((r) => r.id == 'lesserafim');
|
|
expect(score.rank, row.rank, reason: '${score.metric.label} 순위 불일치');
|
|
expect(score.value, row.value, reason: '${score.metric.label} 수치 불일치');
|
|
}
|
|
});
|
|
|
|
test('남의 행성에서는 내 기여 상위 %를 지어내지 않는다 — 대조표 3-5', () async {
|
|
final mine = await const SamplePlanetRepository().load(nebula: false);
|
|
expect(mine.scores.every((s) => s.myTopPercent != null), isTrue);
|
|
|
|
final theirs =
|
|
await SamplePlanetRepository(fandomId: 'aespa').load(nebula: false);
|
|
expect(theirs.scores.every((s) => s.myTopPercent == null), isTrue);
|
|
});
|
|
|
|
test('남의 행성 사용자 랭킹에는 「나」 행이 없다', () async {
|
|
const repo = SamplePlanetRepository();
|
|
expect(
|
|
(await repo.userRanking(metric: FpiMetric.bpm, fandomId: 'bts')).mine,
|
|
isNotNull,
|
|
);
|
|
expect(
|
|
(await repo.userRanking(metric: FpiMetric.bpm, fandomId: 'ive')).mine,
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('ID 를 주지 않으면 내 행성이다 — 기존 호출부가 그대로 돈다', () async {
|
|
final byDefault = await const SamplePlanetRepository().load(nebula: false);
|
|
final explicit =
|
|
await SamplePlanetRepository(fandomId: 'bts').load(nebula: false);
|
|
expect(byDefault.fandomId, 'bts');
|
|
expect(byDefault.totalFpi, explicit.totalFpi);
|
|
});
|
|
});
|
|
|
|
group('지표별 랭킹 — 발주사 검토회신서 4절 회귀 가드', () {
|
|
test('지표를 바꾸면 랭킹 순서가 달라진다', () async {
|
|
final byMetric = <FpiMetric, List<String>>{
|
|
for (final m in FpiMetric.values)
|
|
m: (await repo.planetRanking(metric: m)).rows
|
|
.map((r) => r.name)
|
|
.toList(),
|
|
};
|
|
// 다섯 축의 1위가 모두 같으면 「같은 Ranking 반복」 상태로 돌아간 것이다.
|
|
final tops = byMetric.values.map((rows) => rows.first).toSet();
|
|
expect(tops.length, greaterThan(1), reason: '지표마다 1위가 같습니다');
|
|
// 순서 자체도 축마다 달라야 한다.
|
|
expect(byMetric.values.map((r) => r.join()).toSet(), hasLength(5));
|
|
});
|
|
|
|
test('지표 카드의 값·순위가 행성 랭킹의 내 행성 행과 같다', () async {
|
|
final detail = await repo.load(nebula: false);
|
|
for (final score in detail.scores) {
|
|
final mine = (await repo.planetRanking(metric: score.metric)).mine;
|
|
expect(mine, isNotNull, reason: '${score.metric.label} 내 행성 행 없음');
|
|
expect(mine!.value, score.value, reason: '${score.metric.label} 수치 불일치');
|
|
expect(mine.rank, score.rank, reason: '${score.metric.label} 순위 불일치');
|
|
}
|
|
});
|
|
|
|
test('TOTAL FPI 는 5축 단순 합산이다 — 별첨 B · 설계서 §4.1', () async {
|
|
final detail = await repo.load(nebula: false);
|
|
expect(
|
|
detail.totalFpi,
|
|
detail.scores.fold<int>(0, (a, s) => a + s.value),
|
|
);
|
|
});
|
|
|
|
test('사용자 랭킹도 지표마다 다르다', () async {
|
|
final tops = <String>{
|
|
for (final m in FpiMetric.values)
|
|
(await repo.userRanking(metric: m, fandomId: 'bts')).rows.first.name,
|
|
};
|
|
expect(tops.length, greaterThan(1));
|
|
});
|
|
});
|
|
|
|
group('행성 지표 랭킹 화면 (Figma 546:14589)', () {
|
|
Future<void> pump(WidgetTester tester) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: FpiRankingScreen(metric: FpiMetric.bpm, repository: repo),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('제목은 지표명 + RANKING 이다', (tester) async {
|
|
await pump(tester);
|
|
expect(find.text('BPM RANKING'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('소속 행성 순위를 맨 위에 고정한다', (tester) async {
|
|
await pump(tester);
|
|
expect(find.text('내 행성의 BPM 랭킹'), findsOneWidget);
|
|
// 표본에서 BPM 1위는 NewJeans, 내 행성 BTS 는 2위다(내 줄 카드).
|
|
expect(find.text('BTS'), findsOneWidget);
|
|
expect(find.text('NewJeans'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('행성 행을 누르면 사용자 랭킹으로 간다 (546:14639)', (tester) async {
|
|
await pump(tester);
|
|
await tester.tap(find.text('BTS (나)'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(FpiUserRankingScreen), findsOneWidget);
|
|
expect(find.text('나의 BPM 랭킹'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('사용자 지표 랭킹 화면 (Figma 546:14646)', () {
|
|
Future<void> pump(WidgetTester tester) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: FpiUserRankingScreen(
|
|
metric: FpiMetric.bpm,
|
|
fandomId: 'bts',
|
|
planetName: 'BTS',
|
|
repository: repo,
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('내 순위를 맨 위에 고정한다', (tester) async {
|
|
await pump(tester);
|
|
expect(find.text('나의 BPM 랭킹'), findsOneWidget);
|
|
// 내 줄 카드와 시상대(3위) 양쪽에 「나」가 있다.
|
|
expect(find.text('나'), findsOneWidget);
|
|
expect(find.text('나 (나)'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('필터는 실시간과 국가별 두 축이다', (tester) async {
|
|
await pump(tester);
|
|
await tester.tap(find.text('실시간 랭킹'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('국가별'), findsWidgets);
|
|
expect(find.text('취소'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('국가별로 바꾸면 제목과 나의 랭킹 영역이 국가 기준이 된다', (tester) async {
|
|
await pump(tester);
|
|
await tester.tap(find.text('실시간 랭킹'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('국가별').last);
|
|
await tester.pumpAndSettle();
|
|
// 전 국가 합산이므로 행성명이 없다.
|
|
expect(find.text('BTS'), findsNothing);
|
|
expect(find.text('국가별 랭킹'), findsOneWidget);
|
|
// 「나의 랭킹 영역」(546:14713)은 여기서 내 국가 행이다.
|
|
expect(find.text('나의 BPM 랭킹'), findsNothing);
|
|
expect(find.text('내 국가의 BPM 랭킹'), findsOneWidget);
|
|
// 목록에서도 내 국가 행에 표시가 붙는다.
|
|
expect(find.text('대한민국 (나)'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|