Files
fanuzu/app/test/planet_fx_test.dart
T

204 lines
7.5 KiB
Dart

import 'package:fancake/core/data/planet_visual_state.dart';
import 'package:fancake/core/ui/planet_25d.dart';
import 'package:fancake/features/home/planet_create_screen.dart';
import 'package:fancake/features/galaxy/galaxy_data.dart';
import 'package:fancake/features/galaxy/widgets/galaxy_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('컬러는 마스터(config/planetColors)가 정한다 — 추가 색은 그리고 숨긴 색은 고르지 못한다 (10/3 #6)', () {
final master = planetColorsFrom({'colors': [
{'key': 'gold', 'hex': '#FFC500', 'visible': false},
{'key': 'mint', 'hex': '3EB489'},
{'key': 'bad', 'hex': 'not-a-color'},
]});
expect(master.visible.map((c) => c.$1), ['mint']);
expect(master.all.map((c) => c.$1), ['gold', 'mint']);
setPlanetColorMaster(master.all);
addTearDown(() => setPlanetColorMaster(planetColors));
expect(planetColorOf('mint'), const Color(0xFF3EB489));
expect(planetColorOf('gold'), const Color(0xFFFFC500));
// 마스터가 비면 Figma 기본 8색.
expect(planetColorsFrom(null).visible, planetColors);
});
// C-50 — 생성 화면이 고른 colorKey 를 같은 팔레트로 본체에 입힌다.
testWidgets('행성 본체는 서버 colorKey 의 생성 팔레트 색을 입는다', (tester) async {
expect(identical(PlanetCreateScreen.colors, planetColors), isTrue);
expect(PlanetVisualState.fromDocument({'colorKey': 'gold'}).colorKey, 'gold');
expect(planetColorOf('gold'), const Color(0xFFFFC500));
expect(planetColorOf('violet'), isNull, reason: '서버 기본값은 원래 일러스트 그대로');
Future<Iterable<ColorFiltered>> filters(Color? tint) async {
await tester.pumpWidget(MaterialApp(home: Center(child: Planet25D(
image: 'assets/figma/planet-home.png', size: 200, stage: 1, tint: tint, animate: false,
))));
return tester.widgetList<ColorFiltered>(find.byType(ColorFiltered));
}
expect(await filters(null), isEmpty);
final tinted = await filters(planetColorOf('gold'));
expect(tinted.single.colorFilter,
const ColorFilter.mode(Color(0xFFFFC500), BlendMode.modulate));
});
testWidgets('행성 본체와 로고는 시간이 지나도 평면 회전하지 않는다', (tester) async {
final previous = Planet25D.animationsEnabled;
Planet25D.animationsEnabled = true;
addTearDown(() => Planet25D.animationsEnabled = previous);
await tester.pumpWidget(const MaterialApp(home: Center(child: Planet25D(
image: 'assets/figma/planet-home.png', size: 200, stage: 1,
))));
final body = find.byWidgetPredicate((w) => w is Image &&
w.image is AssetImage && (w.image as AssetImage).assetName == 'assets/figma/planet-home.png');
await tester.pump(const Duration(milliseconds: 1));
for (var quarter = 0; quarter < 4; quarter++) {
await tester.pump(const Duration(seconds: 12));
final box = tester.renderObject<RenderBox>(body);
final left = box.localToGlobal(Offset(0, box.size.height / 2));
final right = box.localToGlobal(Offset(box.size.width, box.size.height / 2));
expect(right.dx - left.dx, greaterThan(0));
expect(right.dy, closeTo(left.dy, 0.01));
}
await tester.pumpWidget(const SizedBox());
});
testWidgets('뒤 고리와 잔해는 본체 안쪽에서 차폐하고 앞 고리만 통과한다', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: Planet25D(
image: 'assets/figma/planet-home.png',
size: 200,
stage: 4,
animate: false,
),
),
),
);
final clips = tester
.widgetList<ClipPath>(
find.descendant(
of: find.byType(Planet25D),
matching: find.byType(ClipPath),
),
)
.where((w) => w.clipper != null)
.toList();
expect(clips.length, 2);
const size = Size(340, 340);
final rear = clips.first.clipper!.getClip(size);
final front = clips.last.clipper!.getClip(size);
expect(rear.contains(const Offset(170, 145)), false, reason: '본체 뒤는 투과 금지');
expect(
rear.contains(const Offset(55, 150)),
true,
reason: '본체 밖의 뒤 고리는 표시',
);
expect(
front.contains(const Offset(170, 195)),
true,
reason: '앞 고리는 본체 위에 합성',
);
expect(tester.takeException(), isNull);
});
testWidgets('갤럭시의 Stage 0도 고체 행성 대신 원본 성운을 렌더한다', (tester) async {
const nebula = PlanetNode(
id: 'nebula',
name: '성운',
stage: 0,
rank: 1,
fpi: 0,
members: 1,
online: 1,
isMine: true,
);
await tester.pumpWidget(
MaterialApp(
home: GalaxyField(
planets: const [nebula],
selected: nebula,
onSelect: (_) {},
spin: 0,
pulse: 0,
),
),
);
expect(find.byType(Planet25D), findsOneWidget);
final images = tester
.widgetList<Image>(find.byType(Image))
.map((image) => image.image)
.whereType<AssetImage>()
.map((asset) => asset.assetName);
expect(images.contains('assets/fx/nebula-figma.png'), true);
expect(images.contains('assets/figma/planet-home.png'), false);
});
testWidgets(
'Stage 0 uses the Figma nebula instead of a solid planet; stages expose their names',
(tester) async {
final semantics = tester.ensureSemantics();
const names = ['성운', '시작', '성장', '함께', '동행', '빛', '영원'];
for (var stage = 0; stage <= 6; stage++) {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Planet25D(
image: 'assets/figma/planet-home.png',
size: 200,
stage: stage,
animate: false,
),
),
),
);
final images = tester
.widgetList<Image>(find.byType(Image))
.map((image) => image.image)
.whereType<AssetImage>()
.map((asset) => asset.assetName);
expect(images.contains('assets/figma/planet-home.png'), stage != 0);
expect(images.contains('assets/fx/nebula-figma.png'), stage == 0);
expect(
find.bySemanticsLabel('Stage $stage · ${names[stage]}'),
findsOneWidget,
);
expect(tester.takeException(), isNull);
}
semantics.dispose();
},
);
testWidgets(
'Reduce Motion stops the ambient ticker, including runtime changes',
(tester) async {
Planet25D.animationsEnabled = true;
addTearDown(() => Planet25D.animationsEnabled = false);
Future<void> show(bool reduced) => tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: MediaQueryData(disableAnimations: reduced),
child: const Center(
child: Planet25D(
image: 'assets/figma/planet-home.png',
size: 200,
),
),
),
),
);
await show(true);
await tester.pump(const Duration(seconds: 1));
expect(tester.binding.transientCallbackCount, 0);
await show(false);
await tester.pump(const Duration(milliseconds: 100));
expect(tester.binding.transientCallbackCount, greaterThan(0));
await show(true);
await tester.pump(const Duration(seconds: 1));
expect(tester.binding.transientCallbackCount, 0);
},
);
}