앱 단위·화면·통합 테스트
This commit is contained in:
@@ -0,0 +1,580 @@
|
||||
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,
|
||||
rank: s.rank,
|
||||
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);
|
||||
// 사유는 배지로도 뜬다 — 숨기지 않는다.
|
||||
expect(find.textContaining('MOCK · '), findsOneWidget);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user