Files
fanuzu/app/test/galaxy_mini_map_test.dart
T

387 lines
12 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/home/widgets/home_hero.dart';
import 'package:fancake/features/planet/planet_data.dart';
import 'package:fancake/features/quest/quest_data.dart';
import 'eclipse_stubs.dart';
import 'galaxy_mini_map_capture_test.dart' show loadAppFonts, mineAt;
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));
});
// 10/4 실기기 — 140pt 에서 HUD 글상자가 미니맵 대부분을 덮어 궤도·행성이 안 보였다.
testWidgets('140 에서는 내 행성 간략 카드만(C-105) — 장면을 덮지 않는 크기', (tester) async {
await tester.runAsync(loadAppFonts);
await pumpMap(tester, _snap(_base));
final p = painterOf(tester);
expect(p.compactText, [
('ALPHA', GalaxyMiniMapPainter.compactNamePt),
('#1 · FPI 5,000', GalaxyMiniMapPainter.compactLinePt),
('LIVE 1,204', GalaxyMiniMapPainter.compactLinePt),
]);
for (final (_, pt) in p.compactText) {
expect(pt, greaterThanOrEqualTo(8));
}
final card = p.compactSize;
expect(card.width, lessThanOrEqualTo(140 * GalaxyMiniMapPainter.hudFloor));
expect(
card.width * card.height,
lessThan(140 * 140 * 0.2),
reason: '카드는 미니맵 넓이의 1/5 미만',
);
expect(p.hudWidthOf('b'), 0, reason: '다른 행성 카드는 뺀다');
expect(p.hudWidthOf('c'), 0);
});
testWidgets('긴 행성 이름은 말줄임 — 순위·FPI·LIVE 숫자는 줄이지 않는다', (tester) async {
await tester.runAsync(loadAppFonts);
await pumpMap(
tester,
_snap(const [
PlanetNode(
id: 'a',
name: 'FANUZU 데모 행성 아주 긴 이름 #1',
rank: 1,
fpi: 123456,
members: 10,
online: 1204,
isMine: true,
),
]),
);
final p = painterOf(tester);
expect(p.compactText.skip(1).map((t) => t.$1), [
'#1 · FPI 123,456',
'LIVE 1,204',
]);
for (final (_, pt) in p.compactText.skip(1)) {
expect(pt, GalaxyMiniMapPainter.compactLinePt);
}
expect(
p.compactSize.width,
lessThanOrEqualTo(140 * GalaxyMiniMapPainter.hudFloor + 0.01),
);
});
testWidgets('내 행성이 1위가 아니어도 첫 화면 가운데에 둔다(C-103)', (tester) async {
await pumpMap(tester, _snap(mineAt(3)));
final p = painterOf(tester);
expect(p.worldOf('3')!.distance, lessThan(1), reason: '내 행성 = 장면 원점');
expect(p.orbitOf('3'), 2 * _spacing, reason: '궤도는 FPI 순위 그대로');
expect(p.worldOf('1')!.distance, greaterThan(_spacing / 2));
});
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);
// C-105 약 140pt · C-113 자리 = Figma 「홈」(2097:16428) 히어로 우측 상단 · 오른쪽 여백 0.
expect(
tester.getSize(mini),
const Size.square(HomeHero.miniMapSize),
);
expect(
tester.getTopRight(mini).dx,
closeTo(tester.getSize(find.byType(HomeScreen)).width, 1),
);
expect(
tester.getTopLeft(mini).dy,
closeTo(
tester.getTopLeft(find.byType(HomeHero)).dy +
MediaQuery.paddingOf(tester.element(mini)).top -
48 + // 사본 y 는 상태 표시줄 48 을 포함한다(HomeHero 와 같은 환산).
HomeHero.miniMapTop,
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);
});
}