341 lines
12 KiB
Dart
341 lines
12 KiB
Dart
import 'package:fancake/core/push/push_messaging.dart';
|
|
import 'package:fancake/core/theme/tokens.dart';
|
|
import 'package:fancake/core/ui/fx_intensity.dart';
|
|
import 'package:fancake/features/eclipse/widgets/live_proof.dart';
|
|
import 'package:fancake/features/galaxy/galaxy_data.dart';
|
|
import 'package:fancake/features/galaxy/widgets/zap_feedback.dart';
|
|
import 'package:fancake/features/galaxy/zap_controller.dart';
|
|
import 'package:fancake/features/notification/notification_screen.dart';
|
|
import 'package:fancake/features/quest/quest_data.dart';
|
|
import 'package:fancake/features/quest/quest_reward_screen.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'eclipse_stubs.dart';
|
|
|
|
/// UX·FX v1.5 2차 약속분(10/8) — 진척현황 부록 B-3 1-2 · 4-2 · 4-3(퀘스트 완료 → 성공 햅틱 · 행성 반응),
|
|
/// 1-4 · 8-6(Push ↔ 라이브 띠 중복 제어 · 알림 위계 · Push 탭 실제 화면 이동), 원장 C-78 단계 배정.
|
|
void main() {
|
|
group('C-78 단계 배정 — Push · 알림함 같은 표', () {
|
|
test('위기 · 공동행동 · 성취 · 결과 · 그 밖', () {
|
|
expect(pushTierOf('eclipse'), PushTier.crisis);
|
|
expect(pushTierOf('eclipse', phase: 'start'), PushTier.crisis);
|
|
expect(pushTierOf('eclipse', phase: 'defended'), PushTier.result);
|
|
expect(pushTierOf('timeattack'), PushTier.collective);
|
|
expect(pushTierOf('evolution'), PushTier.achievement);
|
|
expect(
|
|
pushTierOf('item_used'),
|
|
PushTier.achievement,
|
|
reason: 'HERO TIME 발동 = 성취',
|
|
);
|
|
expect(pushTierOf('quest'), PushTier.result);
|
|
expect(pushTierOf('campaign'), PushTier.result);
|
|
expect(pushTierOf('briefing'), PushTier.other);
|
|
expect(pushTierOf(''), PushTier.other);
|
|
});
|
|
|
|
test('알림함 항목도 같은 단계 — 표본(eventType 없음)은 종류로 읽는다', () {
|
|
AppNotification n(
|
|
NotificationKind kind, {
|
|
String? event,
|
|
String phase = '',
|
|
}) => AppNotification(
|
|
id: 'x',
|
|
kind: kind,
|
|
title: 't',
|
|
at: DateTime(2026),
|
|
eventType: event,
|
|
phase: phase,
|
|
);
|
|
expect(n(NotificationKind.itemUsed).tier, PushTier.achievement);
|
|
expect(
|
|
n(NotificationKind.eclipse, event: 'eclipse', phase: 'defended').tier,
|
|
PushTier.result,
|
|
);
|
|
// 승계 알림처럼 eventType 이 빈 문서는 아이템 사용으로 떨어지지만 위계는 올리지 않는다.
|
|
expect(n(NotificationKind.itemUsed, event: '').tier, PushTier.other);
|
|
});
|
|
});
|
|
|
|
group('Push 탭 → 실제 화면 (v1.5 8장 Deep Link · 9장)', () {
|
|
PushNotice n(
|
|
String kind, {
|
|
String? fandomId,
|
|
String phase = '',
|
|
String? targetPath,
|
|
String? questId,
|
|
String? campaignId,
|
|
}) => PushNotice(
|
|
title: 't',
|
|
body: 'b',
|
|
kind: kind,
|
|
fandomId: fandomId,
|
|
phase: phase,
|
|
targetPath: targetPath,
|
|
questId: questId,
|
|
campaignId: campaignId,
|
|
);
|
|
|
|
test('종류마다 해당 화면', () {
|
|
expect(n('quest', targetPath: 'quests/q1').target, 'quests/q1');
|
|
expect(n('campaign', campaignId: 'c1').target, 'campaigns/c1');
|
|
expect(
|
|
n('timeattack', questId: 'q2', fandomId: 'bts').target,
|
|
'quests/q2',
|
|
);
|
|
expect(n('eclipse', fandomId: 'bts').target, 'eclipse/bts');
|
|
expect(
|
|
n('eclipse', fandomId: 'bts', phase: 'defended').target,
|
|
'planets/bts',
|
|
);
|
|
expect(n('evolution', fandomId: 'bts').target, 'planets/bts');
|
|
expect(n('item_used', fandomId: 'bts').target, 'planets/bts');
|
|
expect(
|
|
n('briefing', fandomId: 'bts').target,
|
|
isNull,
|
|
reason: 'HOME 브리핑 블록이 그 화면',
|
|
);
|
|
expect(n('eclipse').target, isNull, reason: '행성을 모르면 탭만');
|
|
});
|
|
|
|
test('같은 사건은 문구가 달라도 한 번 — 진화 토픽 Push + 참여자 알림함 Push', () {
|
|
final topic = PushNotice(
|
|
title: '진화',
|
|
body: 'a',
|
|
kind: 'evolution',
|
|
fandomId: 'bts',
|
|
);
|
|
const inbox = PushNotice(
|
|
title: 'FANUZU',
|
|
body: 'b',
|
|
kind: 'evolution',
|
|
fandomId: 'bts',
|
|
);
|
|
expect(shouldShowInApp(inbox, topic), isFalse);
|
|
expect(
|
|
shouldShowInApp(
|
|
const PushNotice(
|
|
title: 'FANUZU',
|
|
body: 'b',
|
|
kind: 'evolution',
|
|
fandomId: 'twice',
|
|
),
|
|
topic,
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('라이브 띠가 이미 알리는 사건은 띄우지 않는다 (v1.5 §1.1 Push ↔ 인앱 티커)', () {
|
|
final ticker = liveTickerKey('eclipse', 'bts');
|
|
expect(
|
|
shouldShowInApp(
|
|
n('eclipse', fandomId: 'bts', phase: 'start'),
|
|
null,
|
|
ticker: ticker,
|
|
),
|
|
isFalse,
|
|
);
|
|
expect(
|
|
shouldShowInApp(n('eclipse', fandomId: 'twice'), null, ticker: ticker),
|
|
isTrue,
|
|
reason: '다른 행성',
|
|
);
|
|
expect(
|
|
shouldShowInApp(
|
|
n('eclipse', fandomId: 'bts', phase: 'defended'),
|
|
null,
|
|
ticker: ticker,
|
|
),
|
|
isTrue,
|
|
reason: '방어 성공은 띠에 없다',
|
|
);
|
|
expect(
|
|
shouldShowInApp(
|
|
n('item_used', fandomId: 'bts'),
|
|
null,
|
|
ticker: liveTickerKey('item_used', 'bts'),
|
|
),
|
|
isFalse,
|
|
reason: 'HERO TIME 전광판',
|
|
);
|
|
});
|
|
|
|
test('띠 한 줄은 같은 사건의 Push 종류를 들고 있다', () {
|
|
final start = DateTime.now().subtract(const Duration(seconds: 5));
|
|
TickerLine? line({eclipse, bool hero = false}) => tickerLine(
|
|
eclipse: eclipse,
|
|
heroTime: hero,
|
|
heroNicknames: const [],
|
|
events: const [],
|
|
fandomName: 'BTS',
|
|
now: DateTime.now(),
|
|
fallback: 'f',
|
|
);
|
|
expect(line(eclipse: eclipseFixture(startAt: start))!.push, 'eclipse');
|
|
expect(line(hero: true)!.push, 'item_used');
|
|
expect(line()!.push, isNull);
|
|
});
|
|
});
|
|
|
|
testWidgets('알림함 위계 — 위기 · 공동행동 · 성취만 배지 획 색을 올린다 (v1.5 8장)', (tester) async {
|
|
final now = DateTime.now();
|
|
AppNotification item(
|
|
String id,
|
|
NotificationKind kind,
|
|
String event, {
|
|
String phase = '',
|
|
}) => AppNotification(
|
|
id: id,
|
|
kind: kind,
|
|
title: id,
|
|
at: now,
|
|
eventType: event,
|
|
phase: phase,
|
|
);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: NotificationScreen(
|
|
repository: SampleNotificationRepository(
|
|
items: [
|
|
item(
|
|
'crisis',
|
|
NotificationKind.eclipse,
|
|
'eclipse',
|
|
phase: 'start',
|
|
),
|
|
item('achieve', NotificationKind.itemUsed, 'item_used'),
|
|
item('result', NotificationKind.quest, 'quest'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
Color ring(String title) {
|
|
final row = find
|
|
.ancestor(of: find.text(title), matching: find.byType(Row))
|
|
.last;
|
|
final badge = tester.widget<Container>(
|
|
find.descendant(of: row, matching: find.byType(Container)).first,
|
|
);
|
|
return ((badge.decoration! as BoxDecoration).border! as Border).top.color;
|
|
}
|
|
|
|
expect(ring('crisis'), AppColors.neonMagenta);
|
|
expect(ring('achieve'), AppColors.gold);
|
|
expect(ring('result'), const Color(0xFF333333), reason: 'Figma 기본 획');
|
|
});
|
|
|
|
group('퀘스트 완료 → 성공 햅틱 · 행성 반응 (v1.5 4장)', () {
|
|
test('Music/Location 은 Medium, 그 밖은 Small (v1.5 §1)', () {
|
|
expect(questSuccessFx(QuestType.musicShake), FxIntensity.medium);
|
|
expect(questSuccessFx(QuestType.place), FxIntensity.medium);
|
|
expect(questSuccessFx(QuestType.steps), FxIntensity.small);
|
|
expect(questSuccessFx(QuestType.attend), FxIntensity.small);
|
|
});
|
|
|
|
testWidgets('보상 화면 「확인」이 HOME 행성 반응을 올린다', (tester) async {
|
|
const card = QuestCard(
|
|
id: 'a',
|
|
title: 't',
|
|
categoryName: 'c',
|
|
author: QuestAuthor.admin,
|
|
stage: QuestStage.completed,
|
|
goal: 1,
|
|
progress: 1,
|
|
rewardPop: 20,
|
|
participants: 0,
|
|
);
|
|
tester.view.physicalSize = const Size(1080, 2600);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
const MaterialApp(
|
|
home: QuestRewardScreen(
|
|
detail: QuestDetail(card: card, imageLabel: '', description: ''),
|
|
reward: QuestReward(rewardedPop: 20, balanceAfter: 0),
|
|
),
|
|
),
|
|
);
|
|
final before = questPlanetReaction.value;
|
|
await tester.tap(find.byType(FilledButton));
|
|
await tester.pump();
|
|
expect(questPlanetReaction.value, before + 1);
|
|
});
|
|
|
|
testWidgets('행성 반응 — 에너지 이동 뒤 흡수 · 퀘스트 반영 문구, 잔액은 그대로', (tester) async {
|
|
final zap = ZapController(SampleGalaxyRepository(), onFailure: (_) {});
|
|
addTearDown(zap.dispose);
|
|
zap.balance = 100;
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ListenableBuilder(
|
|
listenable: zap,
|
|
builder: (_, _) => ZapFeedback(
|
|
fandomName: 'BTS',
|
|
quest: zap.questReaction,
|
|
touchSerial: zap.touchSerial,
|
|
pending: zap.pending > 0 || zap.sending,
|
|
result: zap.result,
|
|
source: Offset.zero,
|
|
target: const Offset(100, 100),
|
|
radius: 40,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final reacting = zap.react();
|
|
await tester.pump();
|
|
expect(zap.touchSerial, 1, reason: '먼저 에너지 이동');
|
|
expect(zap.result, isNull, reason: '흡수는 이동 뒤');
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
await reacting;
|
|
await tester.pump();
|
|
expect(find.text('퀘스트 완료가 BTS 성장에 반영되었습니다'), findsOneWidget);
|
|
expect(zap.balance, 100);
|
|
await tester.pump(const Duration(seconds: 2));
|
|
});
|
|
|
|
testWidgets('HOME 반영 문구는 행성 상자 위쪽 — 아래 성장 카드(행성 이름 · 말풍선)를 덮지 않는다(10/8 r11)', (tester) async {
|
|
final zap = ZapController(SampleGalaxyRepository(), onFailure: (_) {});
|
|
addTearDown(zap.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ListenableBuilder(
|
|
listenable: zap,
|
|
builder: (_, _) => ZapFeedback(
|
|
fandomName: 'BTS',
|
|
quest: zap.questReaction,
|
|
touchSerial: zap.touchSerial,
|
|
pending: zap.pending > 0 || zap.sending,
|
|
result: zap.result,
|
|
source: Offset.zero,
|
|
target: const Offset(200, 400),
|
|
radius: 80,
|
|
messageAboveTarget: true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final reacting = zap.react();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
await reacting;
|
|
await tester.pump();
|
|
final text = find.text('퀘스트 완료가 BTS 성장에 반영되었습니다');
|
|
expect(text, findsOneWidget);
|
|
// 행성 상자(반 변 80 / 0.4 / 2 = 100) 윗변 y300 에서 시작해 행성 중심(y400) 위에서 끝난다.
|
|
final box = tester.getRect(
|
|
find.ancestor(of: text, matching: find.byType(DecoratedBox)).first,
|
|
);
|
|
expect(box.top, 300);
|
|
expect(box.bottom, lessThan(400));
|
|
await tester.pump(const Duration(seconds: 2));
|
|
});
|
|
});
|
|
}
|