135 lines
5.0 KiB
Dart
135 lines
5.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:fancake/core/data/repositories.dart'
|
|
show eclipseLook, eclipseWarnWindow;
|
|
import 'package:fancake/core/theme/app_theme.dart';
|
|
import 'package:fancake/core/ui/planet_25d.dart';
|
|
import 'package:fancake/features/eclipse/eclipse_data.dart';
|
|
import 'package:fancake/features/eclipse/eclipse_defend_screen.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'eclipse_stubs.dart';
|
|
|
|
/// UX·FX v1.5 §5 사전 경고 「그림자 접근」 · §5.1 「끊어진 Ring → 완전한 Shield」
|
|
/// (진척현황 부록 B-3 5-1 · 5-4).
|
|
void main() {
|
|
final warn = DateTime(2026, 10, 8, 7, 47);
|
|
final start = DateTime(2026, 10, 8, 7, 50);
|
|
|
|
test('그림자 접근 진행 — 경고 시각 0 → 발생 시각 1, 경고 전 · 창 없음은 null', () {
|
|
expect(eclipseApproach(null, start, warn), isNull);
|
|
expect(eclipseApproach(warn, start, warn.subtract(const Duration(seconds: 1))), isNull);
|
|
expect(eclipseApproach(warn, start, warn), 0);
|
|
expect(eclipseApproach(warn, start, warn.add(const Duration(seconds: 90))), 0.5);
|
|
expect(
|
|
eclipseApproach(warn, start, start.add(const Duration(seconds: 3))),
|
|
1,
|
|
reason: '서버가 회차를 열기 전 지연 동안 그림자가 빠지지 않는다',
|
|
);
|
|
});
|
|
|
|
test('경보 단계 회차 — 이클립스 감소가 아니라 경고 창만', () {
|
|
final scheduled = EclipseState(
|
|
fandomId: 'bts',
|
|
eclipseId: 'ecl-2',
|
|
fandomName: 'BTS',
|
|
erosionRate: 0,
|
|
defense: 0,
|
|
targetDefense: 10,
|
|
defenseCount: 0,
|
|
defended: false,
|
|
scheduled: true,
|
|
warnAt: warn,
|
|
startAt: start,
|
|
endsAt: start.add(const Duration(seconds: 60)),
|
|
);
|
|
expect(eclipseLook(scheduled, warn), (false, false, null));
|
|
expect(eclipseWarnWindow(scheduled), (warn, start));
|
|
expect(eclipseWarnWindow(eclipseFixture()), isNull, reason: '진행 중 회차');
|
|
expect(eclipseWarnWindow(null), isNull);
|
|
});
|
|
|
|
group('행성 그림자', () {
|
|
Widget planet({DateTime? warnAt, DateTime? startAt, bool eclipsed = false}) =>
|
|
MaterialApp(
|
|
home: Center(
|
|
child: Planet25D(
|
|
image: 'assets/figma/planet-home.png',
|
|
size: 200,
|
|
stage: 3,
|
|
isEclipsed: eclipsed,
|
|
eclipseWarnAt: warnAt,
|
|
eclipseStartAt: startAt,
|
|
),
|
|
),
|
|
);
|
|
final now = DateTime.now();
|
|
final w = now.subtract(const Duration(seconds: 90));
|
|
final s = now.add(const Duration(seconds: 90));
|
|
|
|
testWidgets('경고 창 안이면 그림자를 그린다', (tester) async {
|
|
await tester.pumpWidget(planet(warnAt: w, startAt: s));
|
|
expect(find.byKey(const ValueKey('eclipse-shadow')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('경고가 없거나 이미 이클립스 진행 중이면 그리지 않는다', (tester) async {
|
|
await tester.pumpWidget(planet());
|
|
expect(find.byKey(const ValueKey('eclipse-shadow')), findsNothing);
|
|
await tester.pumpWidget(planet(warnAt: w, startAt: s, eclipsed: true));
|
|
expect(find.byKey(const ValueKey('eclipse-shadow')), findsNothing);
|
|
});
|
|
});
|
|
|
|
test('Shield Ring — 처음엔 끊어진 조각, 참여자만큼 이어지고 목표 달성이면 원 하나', () {
|
|
const slot = 2 * math.pi / shieldSegments;
|
|
final broken = shieldArcs(0);
|
|
expect(broken, hasLength(shieldSegments));
|
|
expect(broken.first.$2, closeTo(slot * 0.5, 1e-9));
|
|
expect(shieldArcs(0.5).first.$2, closeTo(slot * 0.75, 1e-9));
|
|
expect(shieldArcs(0.99).first.$2, lessThan(slot));
|
|
expect(shieldArcs(1), [(-math.pi / 2, 2 * math.pi)]);
|
|
});
|
|
|
|
testWidgets('방어 화면 Shield 는 서버 고유 참여자 / 목표 비율을 따른다', (tester) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 2;
|
|
addTearDown(tester.view.reset);
|
|
final shakes = StreamController<DateTime>.broadcast();
|
|
addTearDown(shakes.close);
|
|
final repo = FakeEclipseRepository(
|
|
state: eclipseFixture(
|
|
defense: 3,
|
|
targetDefense: 4,
|
|
startAt: DateTime.now().subtract(const Duration(seconds: 1)),
|
|
endsAt: DateTime.now().add(const Duration(seconds: 59)),
|
|
),
|
|
);
|
|
addTearDown(repo.dispose);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: EclipseDefendScreen(
|
|
repository: repo,
|
|
fandomId: 'bts',
|
|
shakes: () => shakes.stream,
|
|
),
|
|
),
|
|
);
|
|
for (var i = 0; i < 6; i++) {
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
}
|
|
final ring = tester
|
|
.widgetList<CustomPaint>(find.descendant(
|
|
of: find.byKey(const ValueKey('shield-ring')),
|
|
matching: find.byType(CustomPaint),
|
|
))
|
|
.map((p) => p.painter)
|
|
.whereType<ShieldRingPainter>()
|
|
.single;
|
|
expect(ring.progress, 0.75);
|
|
await tester.pumpWidget(const SizedBox());
|
|
});
|
|
}
|