GIVE [+1] · [+10] 은 서버가 기여를 확정한 묶음 기준으로만 표시

This commit is contained in:
Sn-Kinos
2026-10-22 01:26:46 +09:00
parent 2836f07a71
commit 1a7dc711d3
3 changed files with 156 additions and 11 deletions
+123 -5
View File
@@ -20,6 +20,7 @@ class _GatedGalaxy extends SampleGalaxyRepository {
_GatedGalaxy();
final counts = <int>[];
final fandoms = <String>[];
final keys = <String>[];
final gates = <Completer<ZapResult>>[];
@override
@@ -30,6 +31,7 @@ class _GatedGalaxy extends SampleGalaxyRepository {
}) {
counts.add(count);
fandoms.add(fandomId);
keys.add(clientSeq);
gates.add(Completer());
return gates.last.future;
}
@@ -78,6 +80,56 @@ void main() {
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: (_) {});
@@ -109,7 +161,10 @@ void main() {
});
group('HOME 행성 GIVE 반응', () {
Future<void> pumpHome(WidgetTester tester) async {
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);
@@ -120,7 +175,7 @@ void main() {
repository: const SampleHomeRepository(),
questRepository: const SampleQuestRepository(),
eclipse: const NoEclipseRepository(),
galaxyRepository: const _OkGalaxy(),
galaxyRepository: galaxy,
planetRepository: samplePlanetRepository,
),
),
@@ -135,19 +190,22 @@ void main() {
.getTranslation()
.x;
testWidgets('GIVE 1회: 행성이 흔들리고 행성 우하단에 [+1] 이 0.5초 뜬다', (tester) async {
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: 450));
await tester.pump(const Duration(milliseconds: 550));
expect(label, findsNothing);
await tester.pump(const Duration(seconds: 3));
});
@@ -163,17 +221,77 @@ void main() {
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(find.text('+10'), findsOneWidget);
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,