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> 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(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(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( 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(find.byType(Image)) .map((image) => image.image) .whereType() .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(find.byType(Image)) .map((image) => image.image) .whereType() .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 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); }, ); }