Files
fanuzu/app/test/galaxy_mini_map_test.dart
T

328 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fancake/core/data/planet_visual_state.dart';
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/galaxy_screen.dart';
import 'package:fancake/features/galaxy/widgets/galaxy_mini_map.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/quest/quest_data.dart';
import 'eclipse_stubs.dart';
import 'perf_paint_test.dart' show medianPaintMicros, paintBudgetMicros;
/// 프로토타입 궤도 간격 450 × Mass Volume 1.5 × Orbit Exp 1.0.
const _spacing = 675.0;
GalaxySnapshot _snap(List<PlanetNode> planets) =>
GalaxySnapshot(planets: planets, myPop: 0);
const _base = [
PlanetNode(
id: 'a',
name: 'Alpha',
rank: 1,
fpi: 5000,
members: 10,
online: 1204,
isMine: true,
),
PlanetNode(
id: 'b',
name: 'Beta',
rank: 2,
fpi: 4000,
members: 10,
online: 30,
),
PlanetNode(
id: 'c',
name: 'Gamma',
rank: 3,
fpi: 1000,
members: 10,
online: 20,
),
];
void main() {
late int opened;
Future<void> pumpMap(
WidgetTester tester,
GalaxySnapshot data, {
double size = 140,
}) => tester.pumpWidget(
MaterialApp(
home: Center(
child: GalaxyMiniMap(data: data, onOpen: () => opened++, size: size),
),
),
);
GalaxyMiniMapPainter painterOf(WidgetTester tester) => tester
.widgetList<CustomPaint>(
find.descendant(
of: find.byType(GalaxyMiniMap),
matching: find.byType(CustomPaint),
),
)
.map((w) => w.painter)
.whereType<GalaxyMiniMapPainter>()
.single;
setUp(() => opened = 0);
testWidgets('실데이터의 내 행성 HUD 숫자를 그리고 궤도는 순위로 정한다', (tester) async {
await pumpMap(tester, _snap(_base));
final label = tester.getSemantics(find.byType(GalaxyMiniMap)).label;
expect(label, contains('ALPHA · RANK #1 · FPI: 5000 · LIVE: 1,204'));
final p = painterOf(tester);
expect(p.orbitOf('a'), 0);
expect(p.orbitOf('b'), _spacing);
expect(p.orbitOf('c'), 2 * _spacing);
expect(tester.takeException(), isNull);
});
testWidgets('행성 색은 팬덤 colorKey 를 따르고, 없거나 모르는 키는 순번 색으로 둔다', (tester) async {
await pumpMap(
tester,
_snap([
for (final (i, p) in _base.indexed)
PlanetNode(
id: p.id,
name: p.name,
rank: p.rank,
fpi: p.fpi,
members: p.members,
online: p.online,
isMine: p.isMine,
colorKey: const ['gold', 'violet', null][i],
),
]),
);
final p = painterOf(tester);
expect(p.colorOf('a'), planetColorOf('gold'));
// 모르는 키(서버 기본값 violet)·미제공은 FPI 순번 프로토타입 색(2번째 · 3번째).
expect(p.colorOf('b'), const Color(0xFF8B5CF6));
expect(p.colorOf('c'), const Color(0xFF10B981));
});
testWidgets('누르면 열리고, 가로로 끌면 장면이 돈다(±180° 한계)', (tester) async {
await pumpMap(tester, _snap(_base));
await tester.tap(find.byType(GalaxyMiniMap));
expect(opened, 1);
await tester.drag(find.byType(GalaxyMiniMap), const Offset(30, 0));
await tester.pump();
final turned = painterOf(tester).rotation;
expect(turned, greaterThan(0));
await tester.drag(find.byType(GalaxyMiniMap), const Offset(-2000, 0));
await tester.pump();
expect(painterOf(tester).rotation, closeTo(-math.pi, 1e-9));
expect(opened, 1, reason: '끌기는 열기가 아니다');
});
testWidgets('순위가 바뀌면 궤도와 HUD 숫자가 따라 바뀐다', (tester) async {
await pumpMap(tester, _snap(_base));
await pumpMap(
tester,
_snap(const [
PlanetNode(
id: 'c',
name: 'Gamma',
rank: 1,
fpi: 6000,
members: 10,
online: 20,
),
PlanetNode(
id: 'a',
name: 'Alpha',
rank: 2,
fpi: 5100,
members: 10,
online: 1300,
isMine: true,
),
PlanetNode(
id: 'b',
name: 'Beta',
rank: 3,
fpi: 4000,
members: 10,
online: 30,
),
]),
);
final p = painterOf(tester);
expect(p.orbitOf('c'), 0);
expect(p.orbitOf('a'), _spacing);
expect(
tester.getSemantics(find.byType(GalaxyMiniMap)).label,
contains('ALPHA · RANK #2 · FPI: 5100 · LIVE: 1,300'),
);
});
testWidgets('움직임이 켜져 있으면 새 궤도로 미끄러져 간다(프로토타입 lerp)', (tester) async {
Planet25D.animationsEnabled = true;
addTearDown(() => Planet25D.animationsEnabled = false);
await pumpMap(tester, _snap(_base));
// 한 프레임 dt 는 1/20 초로 잘리므로 프레임 단위로 흘린다.
for (var i = 0; i < 30; i++) {
await tester.pump(const Duration(milliseconds: 16));
}
final mid = painterOf(tester).orbitOf('c')!;
expect(mid, greaterThan(0));
expect(mid, lessThan(2 * _spacing));
for (var i = 0; i < 600; i++) {
await tester.pump(const Duration(milliseconds: 16));
}
expect(painterOf(tester).orbitOf('c'), closeTo(2 * _spacing, 5));
});
testWidgets('140 에서는 내 행성 간략 카드만 — 두 줄 모두 9pt 이상', (tester) async {
await pumpMap(tester, _snap(_base));
final p = painterOf(tester);
expect(p.compactText, [
('ALPHA', GalaxyMiniMapPainter.compactNamePt),
('#1 · FPI 5,000 · LIVE 1,204', GalaxyMiniMapPainter.compactLinePt),
]);
expect(p.compactText.first.$2, greaterThanOrEqualTo(10));
for (final (_, pt) in p.compactText) {
expect(pt, greaterThanOrEqualTo(9));
}
final w = p.hudWidthOf('a')!;
expect(w, greaterThan(0));
expect(w, lessThanOrEqualTo(140 * GalaxyMiniMapPainter.hudFloor));
expect(p.hudWidthOf('b'), 0, reason: '다른 행성 카드는 뺀다');
expect(p.hudWidthOf('c'), 0);
});
testWidgets('320 에서는 모든 행성에 프로토타입 카드를 그린다', (tester) async {
await pumpMap(tester, _snap(_base), size: 320);
final p = painterOf(tester);
for (final id in ['a', 'b', 'c']) {
expect(p.hudWidthOf(id), greaterThan(0), reason: id);
}
expect(p.compactText, isEmpty);
});
testWidgets('FPI 상위 10개 + 순위 밖 내 행성만 그린다', (tester) async {
await pumpMap(
tester,
_snap([
for (var i = 0; i < 12; i++)
PlanetNode(
id: 'p$i',
name: 'P$i',
rank: i + 1,
fpi: 1000 - i,
members: 1,
online: 1,
isMine: i == 11,
),
]),
);
final p = painterOf(tester);
expect(p.orbitOf('p9'), isNotNull);
expect(p.orbitOf('p10'), isNull);
expect(p.orbitOf('p11'), isNotNull);
});
testWidgets('보조 기술은 버튼 하나로 읽고 눌러 연다', (tester) async {
final handle = tester.ensureSemantics();
await pumpMap(tester, _snap(_base));
final node = tester.getSemantics(find.byType(GalaxyMiniMap));
expect(node.getSemanticsData().hasAction(SemanticsAction.tap), isTrue);
node.owner!.performAction(node.id, SemanticsAction.tap);
expect(opened, 1);
handle.dispose();
});
testWidgets('게이지 조절 UI 는 없다(C-51)', (tester) async {
await pumpMap(tester, _snap(_base));
expect(find.byType(Slider), findsNothing);
expect(find.byType(RangeSlider), findsNothing);
for (final gauge in [
'Mass Volume',
'Orbit Exp.',
'HUD Size',
'HUD Dist.',
'Camera Height',
'View Rotation',
'Action Flare',
]) {
expect(find.text(gauge), findsNothing);
}
});
test('행성 12개 한 프레임 그리기가 예산 안에 든다', () {
final painter = GalaxyMiniMapPainter()
..update([
for (var i = 0; i < 12; i++)
PlanetNode(
id: 'p$i',
name: 'PLANET $i',
rank: i + 1,
fpi: 300000 - i * 20000,
members: 100,
online: 10,
isMine: i == 2,
),
])
..settle();
addTearDown(painter.dispose);
final micros = medianPaintMicros(painter, const Size(140, 140));
expect(micros, lessThan(paintBudgetMicros));
});
testWidgets('HOME 미니맵은 같은 저장소의 Galaxy로 진입한다', (tester) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 3;
addTearDown(tester.view.reset);
const galaxy = SampleGalaxyRepository();
final states = StreamController<PlanetVisualState>.broadcast();
addTearDown(states.close);
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(),
home: HomeScreen(
repository: const SampleHomeRepository(),
questRepository: const SampleQuestRepository(),
eclipse: const NoEclipseRepository(),
galaxyRepository: galaxy,
planetRepository: samplePlanetRepository,
watchPlanet: (_) => states.stream,
),
),
);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
final mini = find.byType(GalaxyMiniMap);
expect(mini, findsOneWidget);
// Q-DS-06 권고안 A(회신 전 임시) — 행성 영역 우측 상단 140 정사각.
expect(tester.getSize(mini), const Size(140, 140));
expect(
tester.getTopRight(mini).dx,
closeTo(tester.getSize(find.byType(HomeScreen)).width - 16, 1),
);
await tester.tap(mini);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(
tester.widget<GalaxyScreen>(find.byType(GalaxyScreen)).repository,
same(galaxy),
);
expect(tester.takeException(), isNull);
await tester.pumpWidget(const SizedBox());
expect(states.hasListener, isFalse);
});
}