Files
fanuzu/app/test/eclipse_stubs.dart
T

216 lines
6.7 KiB
Dart

import 'dart:async';
import 'package:fancake/features/eclipse/eclipse_data.dart';
import 'package:fancake/features/galaxy/galaxy_data.dart';
/// 활성 이클립스가 없는 상태.
///
/// **이클립스와 무관한 화면 검증의 기본값이다.** [SampleEclipseRepository] 는 목
/// 모드 시연을 위해 늘 활성 이클립스를 내므로, 그것을 쓰면 배너와 라이브 띠가
/// 화면 상단을 차지해 F1·F2·F3 의 레이아웃 검증이 이클립스 유무에 딸려 흔들린다.
class NoEclipseRepository implements EclipseRepository {
const NoEclipseRepository();
@override
Stream<EclipseState?> watch({String? fandomId, bool includeResults = false, bool includeScheduled = false}) => Stream.value(null);
@override
Stream<List<LiveEvent>> liveEvents({String? fandomId, int limit = 20}) =>
Stream.value(const []);
@override
Future<DefendResult> defend({
required String fandomId,
required String eclipseId,
required String clientSeq,
required int shakes,
}) async =>
const DefendResult(accepted: 0, clamped: 0, myTotal: 0, duplicate: false);
}
/// 값을 밖에서 밀어 넣는 저장소. 스트림이 바뀔 때 화면이 따라오는지 본다.
///
/// 방어 호출은 실제로 나가지 않고 [calls] 에 기록만 한다 — 연타가 **몇 번의
/// 호출로 묶였는지**와 `count` 합이 맞는지가 검증 대상이다.
class FakeEclipseRepository implements EclipseRepository {
FakeEclipseRepository({EclipseState? state}) {
if (state != null) push(state);
}
final _eclipse = StreamController<EclipseState?>.broadcast();
final _events = StreamController<List<LiveEvent>>.broadcast();
/// 마지막으로 밀어 넣은 값. 새 구독자가 곧바로 받는다.
EclipseState? _last;
List<LiveEvent> _lastEvents = const [];
/// (eclipseId, clientSeq, shakes) 기록.
final List<(String, String, int)> calls = [];
/// 다음 방어 호출이 던질 실패. null 이면 성공한다.
Object? failWith;
/// 서버가 받아들였다고 답할 양. null 이면 보낸 값을 그대로 받아들인다.
int? acceptedOverride;
void push(EclipseState? state) {
_last = state;
if (_eclipse.hasListener) _eclipse.add(state);
}
void pushEvents(List<LiveEvent> events) {
_lastEvents = events;
if (_events.hasListener) _events.add(events);
}
Future<void> dispose() async {
await _eclipse.close();
await _events.close();
}
@override
Stream<EclipseState?> watch({String? fandomId, bool includeResults = false, bool includeScheduled = false}) async* {
yield _last;
yield* _eclipse.stream;
}
@override
Stream<List<LiveEvent>> liveEvents({String? fandomId, int limit = 20}) async* {
yield _lastEvents;
yield* _events.stream;
}
@override
Future<DefendResult> defend({
required String fandomId,
required String eclipseId,
required String clientSeq,
required int shakes,
}) async {
calls.add((eclipseId, clientSeq, shakes));
final failure = failWith;
if (failure != null) throw failure;
final accepted = acceptedOverride ?? shakes;
return DefendResult(
accepted: accepted,
clamped: shakes - accepted,
myTotal: calls.fold(0, (a, c) => a + c.$3),
duplicate: false,
);
}
}
/// 검증에 쓰는 이클립스 한 건. 값은 인자로 덮는다.
EclipseState eclipseFixture({
String fandomId = 'bts',
String eclipseId = 'ecl-1',
int erosionRate = 40,
int defense = 60_000,
int targetDefense = 100_000,
int defenseCount = 12,
bool defended = false,
EclipseResult? result,
DateTime? startAt,
bool scheduled = false,
DateTime? endsAt,
}) => EclipseState(
fandomId: fandomId,
eclipseId: eclipseId,
fandomName: 'BTS',
erosionRate: erosionRate,
defense: defense,
targetDefense: targetDefense,
defenseCount: defenseCount,
defended: defended,
result: result,
startAt: startAt,
scheduled: scheduled,
// 마감이 없으면 화면이 「지났는가」를 물을 수 없다. 기본을 넉넉히 둔다.
endsAt: endsAt ?? DateTime.now().add(const Duration(minutes: 20)),
);
/// 구독을 몇 번 열었고 몇 개가 살아 있는지 센다.
///
/// 실시간 동기화의 두 고장은 **끊지 않는 것**(백그라운드에서 읽기 비용이 계속
/// 든다)과 **다시 열지 않는 것**(복귀한 화면이 잠들기 전 값을 그린다)이다. 둘 다
/// 화면만 보고는 구분되지 않으므로 구독 수를 직접 센다.
class CountingEclipseRepository implements EclipseRepository {
CountingEclipseRepository(this.state);
final EclipseState? state;
/// 지금까지 열린 구독 수(누적).
int subscriptions = 0;
/// 아직 살아 있는 구독 수.
int active = 0;
@override
Stream<EclipseState?> watch({String? fandomId, bool includeResults = false, bool includeScheduled = false}) {
late StreamController<EclipseState?> controller;
controller = StreamController<EclipseState?>(
onListen: () {
subscriptions += 1;
active += 1;
controller.add(state);
},
onCancel: () {
active -= 1;
},
);
return controller.stream;
}
@override
Stream<List<LiveEvent>> liveEvents({String? fandomId, int limit = 20}) =>
Stream.value(const []);
@override
Future<DefendResult> defend({
required String fandomId,
required String eclipseId,
required String clientSeq,
required int shakes,
}) async =>
const DefendResult(accepted: 0, clamped: 0, myTotal: 0, duplicate: false);
}
/// ZAP(행성 성장 기여) 호출을 기록하는 갤럭시 저장소.
///
/// 별첨 B **B-23** — ZAP 은 이클립스 방어가 아니라 행성 성장 기여다. 이 스텁으로
/// 「ZAP 이 방어 함수가 아니라 POP 소비 경로로 나가는가」를 본다.
class RecordingGalaxyRepository implements GalaxyRepository {
RecordingGalaxyRepository({this.snapshot});
/// 돌려줄 스냅샷. 없으면 표본을 쓴다.
final GalaxySnapshot? snapshot;
/// (fandomId, clientSeq, count) 기록.
final List<(String, String, int)> zaps = [];
/// 서버가 확정했다고 답할 잔액. -1 이면 「모름」이다.
int balanceAfter = -1;
/// 다음 호출이 던질 실패. null 이면 성공한다.
Object? failWith;
@override
Future<void> setPrimary(String fandomId) async {}
@override
Future<GalaxySnapshot> load() async =>
snapshot ?? await const SampleGalaxyRepository().load();
@override
Future<ZapResult> zap({
required String fandomId,
required int count,
required String clientSeq,
}) async {
zaps.add((fandomId, clientSeq, count));
final failure = failWith;
if (failure != null) throw failure;
return ZapResult(txId: 'tx-${zaps.length}', balanceAfter: balanceAfter);
}
}