387 lines
12 KiB
Dart
387 lines
12 KiB
Dart
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);
|
||
});
|
||
}
|