322 lines
13 KiB
Dart
322 lines
13 KiB
Dart
import 'dart:async';
|
|
|
|
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/widgets/zap_button.dart';
|
|
import 'package:fancake/features/galaxy/zap_controller.dart';
|
|
import 'package:fancake/features/home/home_data.dart';
|
|
import 'package:fancake/features/home/home_screen.dart';
|
|
import 'package:fancake/features/planet/planet_data.dart';
|
|
import 'package:fancake/features/planet/planet_detail_screen.dart';
|
|
import 'package:fancake/features/quest/quest_data.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'eclipse_stubs.dart';
|
|
|
|
/// 서버 응답을 시험이 직접 풀어 주는 기여 저장소.
|
|
class _GatedGalaxy extends SampleGalaxyRepository {
|
|
_GatedGalaxy();
|
|
final counts = <int>[];
|
|
final fandoms = <String>[];
|
|
final keys = <String>[];
|
|
final gates = <Completer<ZapResult>>[];
|
|
|
|
@override
|
|
Future<ZapResult> zap({
|
|
required String fandomId,
|
|
required int count,
|
|
required String clientSeq,
|
|
}) {
|
|
counts.add(count);
|
|
fandoms.add(fandomId);
|
|
keys.add(clientSeq);
|
|
gates.add(Completer());
|
|
return gates.last.future;
|
|
}
|
|
}
|
|
|
|
/// 바로 확정하는 기여 저장소.
|
|
class _OkGalaxy extends SampleGalaxyRepository {
|
|
const _OkGalaxy();
|
|
|
|
@override
|
|
Future<ZapResult> zap({
|
|
required String fandomId,
|
|
required int count,
|
|
required String clientSeq,
|
|
}) async => ZapResult(txId: clientSeq, balanceAfter: 100000);
|
|
}
|
|
|
|
/// Figma `2033:15818` 행성 리액션 1 · `2024:20758` 행성 2단계 — GIVE 1회 흔들림 1번 · [+1] 0.5초(연속 가능),
|
|
/// 10회 이상 흔들림 x2 · 자전 1.5배 · [+10].
|
|
void main() {
|
|
group('ZapController 연타', () {
|
|
testWidgets('전송 중에 누른 GIVE 도 버리지 않고 다음 묶음으로 보낸다', (tester) async {
|
|
final repo = _GatedGalaxy();
|
|
final zap = ZapController(repo, onFailure: (_) {});
|
|
for (var i = 0; i < 3; i++) {
|
|
zap.tap('f1');
|
|
}
|
|
await tester.pump(const Duration(milliseconds: 450));
|
|
expect(repo.counts, [3]);
|
|
expect(zap.sending, isTrue);
|
|
|
|
zap.tap('f1');
|
|
zap.tap('f1');
|
|
expect(zap.pending, 2);
|
|
expect(zap.queued, 5);
|
|
|
|
await tester.pump(const Duration(milliseconds: 450));
|
|
expect(repo.counts, [3], reason: '앞 묶음이 확정되기 전에는 다음 묶음을 보내지 않는다');
|
|
|
|
repo.gates.first.complete(const ZapResult(txId: 'a', balanceAfter: 10));
|
|
await tester.pump();
|
|
expect(repo.counts, [3, 2]);
|
|
repo.gates.last.complete(const ZapResult(txId: 'b', balanceAfter: 8));
|
|
await tester.pump();
|
|
expect(zap.queued, 0);
|
|
zap.dispose();
|
|
});
|
|
|
|
testWidgets('연타 N회는 묶음이 여러 번이어도 정확히 N회분만 보내고 멱등키가 겹치지 않는다', (tester) async {
|
|
final repo = _GatedGalaxy();
|
|
final zap = ZapController(repo, onFailure: (_) {});
|
|
for (var i = 0; i < 17; i++) {
|
|
zap.tap('f1');
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
// 응답은 오는 대로 확정한다.
|
|
for (final g in repo.gates) {
|
|
if (!g.isCompleted) g.complete(ZapResult(txId: 't${repo.gates.indexOf(g)}', balanceAfter: 0));
|
|
}
|
|
await tester.pump();
|
|
}
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
for (final g in repo.gates) {
|
|
if (!g.isCompleted) g.complete(const ZapResult(txId: 'last', balanceAfter: 0));
|
|
}
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(repo.counts.fold<int>(0, (a, b) => a + b), 17);
|
|
expect(repo.keys.toSet(), hasLength(repo.keys.length));
|
|
expect(zap.queued, 0);
|
|
zap.dispose();
|
|
});
|
|
|
|
testWidgets('전송이 실패하면 보내는 중 묶음과 뒤에 쌓인 연타가 표시 잔액에서 빠지지 않는다(잔액 복원)', (tester) async {
|
|
final repo = _GatedGalaxy();
|
|
final failures = <String>[];
|
|
final zap = ZapController(repo, onFailure: failures.add);
|
|
zap.tap('f1');
|
|
zap.tap('f1');
|
|
await tester.pump(const Duration(milliseconds: 450));
|
|
zap.tap('f1');
|
|
expect(zap.queued, 3);
|
|
repo.gates.single.completeError(StateError('down'));
|
|
await tester.pump();
|
|
expect(failures, hasLength(1));
|
|
expect(zap.queued, 0, reason: '확정 안 된 연타를 잔액에서 미리 빼 두지 않는다');
|
|
expect(zap.needsRetry, isTrue);
|
|
zap.tap('f1');
|
|
expect(zap.pending, 0, reason: '실패 묶음을 다시 확인하기 전에는 새 연타를 받지 않는다');
|
|
zap.retry();
|
|
await tester.pump();
|
|
expect(repo.keys[1], repo.keys[0], reason: '재확인은 같은 멱등키');
|
|
expect(repo.counts[1], 2);
|
|
repo.gates.last.complete(const ZapResult(txId: 'ok', balanceAfter: 5));
|
|
await tester.pump();
|
|
expect(repo.counts, [2, 2]);
|
|
zap.dispose();
|
|
});
|
|
|
|
testWidgets('보내는 중에 다른 행성으로 누른 연타가 앞 행성으로 가지 않는다', (tester) async {
|
|
final repo = _GatedGalaxy();
|
|
final zap = ZapController(repo, onFailure: (_) {});
|
|
zap.tap('a');
|
|
await tester.pump(const Duration(milliseconds: 450));
|
|
zap.tap('b');
|
|
zap.tap('a');
|
|
expect(zap.pending, 1, reason: '쌓인 b 묶음에 a 를 섞지 않는다');
|
|
await tester.pump(const Duration(milliseconds: 450));
|
|
repo.gates.first.complete(const ZapResult(txId: 'a1', balanceAfter: 10));
|
|
await tester.pump();
|
|
expect(repo.fandoms, ['a', 'b']);
|
|
repo.gates.last.complete(const ZapResult(txId: 'b1', balanceAfter: 9));
|
|
await tester.pump();
|
|
zap.dispose();
|
|
});
|
|
|
|
testWidgets('연속 횟수는 [+1] 이 떠 있는 0.5초 안에 다시 누를 때만 이어진다', (tester) async {
|
|
final zap = ZapController(_GatedGalaxy(), onFailure: (_) {});
|
|
zap.tap('f1');
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
zap.tap('f1');
|
|
expect(zap.streak, 2);
|
|
await tester.pump(const Duration(milliseconds: 600));
|
|
zap.tap('f1');
|
|
expect(zap.streak, 1);
|
|
zap.dispose();
|
|
});
|
|
});
|
|
|
|
group('HOME 행성 GIVE 반응', () {
|
|
Future<void> pumpHome(
|
|
WidgetTester tester, [
|
|
GalaxyRepository galaxy = const _OkGalaxy(),
|
|
]) async {
|
|
tester.view.physicalSize = const Size(1125, 2436);
|
|
tester.view.devicePixelRatio = 3;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: HomeScreen(
|
|
repository: const SampleHomeRepository(),
|
|
questRepository: const SampleQuestRepository(),
|
|
eclipse: const NoEclipseRepository(),
|
|
galaxyRepository: galaxy,
|
|
planetRepository: samplePlanetRepository,
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1));
|
|
}
|
|
|
|
double shake(WidgetTester tester) => tester
|
|
.widget<Transform>(find.byKey(const ValueKey('planet-give-shake')))
|
|
.transform
|
|
.getTranslation()
|
|
.x;
|
|
|
|
testWidgets('GIVE 1회: 행성은 바로 흔들리고, 서버 확정 뒤 행성 우하단에 [+1] 이 0.5초 뜬다', (tester) async {
|
|
await pumpHome(tester);
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
expect(shake(tester), isNot(0));
|
|
final label = find.byKey(const ValueKey('planet-give-count'));
|
|
expect(label, findsNothing, reason: '확정 전에는 기여 결과를 띄우지 않는다');
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
await tester.pump();
|
|
expect(tester.widget<Text>(label).data, '+1');
|
|
final planet = tester.getRect(find.byType(Planet25D).first);
|
|
final at = tester.getCenter(label);
|
|
expect(at.dx, greaterThan(planet.center.dx));
|
|
expect(at.dy, greaterThan(planet.center.dy));
|
|
await tester.pump(const Duration(milliseconds: 550));
|
|
expect(label, findsNothing);
|
|
await tester.pump(const Duration(seconds: 3));
|
|
});
|
|
|
|
testWidgets('행성 터치 직후 GIVE 가 흔들림을 다시 시작해도 커뮤니티 이동은 간다', (tester) async {
|
|
await pumpHome(tester);
|
|
await tester.tap(find.byType(Planet25D).first, warnIfMissed: false);
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
await tester.pump(const Duration(seconds: 1));
|
|
expect(find.byType(PlanetDetailScreen), findsOneWidget);
|
|
await tester.pump(const Duration(seconds: 3));
|
|
});
|
|
|
|
testWidgets('확정 전에는 결과를 띄우지 않고, 실패하면 끝까지 띄우지 않으며 표시 잔액을 되돌린다', (tester) async {
|
|
final repo = _GatedGalaxy();
|
|
await pumpHome(tester, repo);
|
|
int shown() => tester.widget<ZapButton>(find.byType(ZapButton)).myPop;
|
|
final before = shown();
|
|
final unit = tester.widget<ZapButton>(find.byType(ZapButton)).zapPop;
|
|
expect(unit, greaterThan(0));
|
|
final label = find.byKey(const ValueKey('planet-give-count'));
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump();
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump(const Duration(milliseconds: 600));
|
|
expect(repo.counts, [2]);
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump();
|
|
expect(shown(), before - 3 * unit, reason: '보내는 중 2회 + 쌓인 1회');
|
|
expect(label, findsNothing, reason: '보냈어도 확정 전에는 띄우지 않는다');
|
|
repo.gates.single.completeError(StateError('down'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
expect(label, findsNothing);
|
|
expect(shown(), before, reason: '실패하면 확정 안 된 연타를 잔액에서 빼지 않는다');
|
|
// 같은 키 재확인이 성공하면 서버 잔액 · [+1].
|
|
await tester.tap(find.text('같은 기여 다시 확인'));
|
|
await tester.pump();
|
|
expect(repo.counts, [2, 2]);
|
|
repo.gates.last.complete(ZapResult(txId: 'ok', balanceAfter: before - 2 * unit));
|
|
await tester.pump();
|
|
expect(shown(), before - 2 * unit);
|
|
expect(tester.widget<Text>(label).data, '+1');
|
|
await tester.pump(const Duration(seconds: 3));
|
|
});
|
|
|
|
testWidgets('10번째 탭이 아직 확정되지 않았으면 앞 묶음 확정에 [+10] 을 띄우지 않는다', (tester) async {
|
|
final repo = _GatedGalaxy();
|
|
await pumpHome(tester, repo);
|
|
for (var i = 0; i < 3; i++) {
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
}
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
expect(repo.counts, [3]);
|
|
for (var i = 0; i < 8; i++) {
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
}
|
|
repo.gates.single.complete(const ZapResult(txId: 'a', balanceAfter: 0));
|
|
await tester.pump();
|
|
expect(find.text('+1'), findsOneWidget);
|
|
expect(find.text('+10'), findsNothing);
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
repo.gates.last.complete(const ZapResult(txId: 'b', balanceAfter: 0));
|
|
await tester.pump();
|
|
expect(find.text('+10'), findsOneWidget);
|
|
await tester.pump(const Duration(seconds: 3));
|
|
});
|
|
|
|
testWidgets('GIVE 10회 이상: [+10] · 자전 1.5배(Planet25D.spinBoost)', (tester) async {
|
|
await pumpHome(tester);
|
|
for (var i = 0; i < 10; i++) {
|
|
await tester.tap(find.byType(ZapButton));
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
}
|
|
expect(
|
|
tester.widget<Planet25D>(find.byType(Planet25D).first).spinBoost,
|
|
isTrue,
|
|
);
|
|
// 마지막 묶음(9 · 10번째)이 확정되면 [+10].
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
await tester.pump();
|
|
expect(find.text('+10'), findsOneWidget);
|
|
await tester.pump(const Duration(milliseconds: 600));
|
|
expect(
|
|
tester.widget<Planet25D>(find.byType(Planet25D).first).spinBoost,
|
|
isFalse,
|
|
);
|
|
await tester.pump(const Duration(seconds: 3));
|
|
});
|
|
});
|
|
|
|
testWidgets('Stage 6 은 자기장 라인이 행성을 감싼다(Figma 2024:20821) · Stage 5 는 없다', (tester) async {
|
|
Future<void> pumpStage(int stage) => tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Center(
|
|
child: Planet25D(
|
|
image: 'assets/figma/planet-home.png',
|
|
size: 200,
|
|
stage: stage,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await pumpStage(6);
|
|
expect(find.byKey(const ValueKey('planet-field-lines')), findsOneWidget);
|
|
await pumpStage(5);
|
|
expect(find.byKey(const ValueKey('planet-field-lines')), findsNothing);
|
|
});
|
|
}
|