// 🗂 Palet kartının YERLEŞİM sözleşmesi (regresyon testi). /// /// Kullanıcı raporu: "kart sayfanın yarısını kaplıyor". Kök neden /// `Flexible` + `ListView` ikilisinin *tight* yerleşimiydi: liste verilen tüm /// yüksekliği dolduruyor, kart doğal boyunu (≈60px) değil kalan alanı /// (≈200px) kaplıyordu. Bu test sayısal olarak ölçer. library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:fikocatia/models/catia_status.dart'; import 'package:fikocatia/screens/home_screen.dart'; import 'package:fikocatia/screens/palette_screen.dart'; import 'package:fikocatia/services/catia_provider.dart'; import 'package:fikocatia/services/palette_state.dart'; import 'package:fikocatia/services/palette_tile.dart'; import 'package:fikocatia/theme/app_theme.dart'; import 'package:fikocatia/widgets/palette_tile_card.dart'; /// Gerçek `paletteTileViews` çıktısına yakın, makro/dökümanı OLMAYAN kart /// (Aşama 1'in "hedef CATIA'nın kartı" hali — yani ekranda görünen). const _tile = PaletteTileView( id: 'CATIA-01', title: 'CATIA-01', fileName: 'Wing.CATPart', status: CatiaStatus.idle, pid: 4242, connected: true, activeWindow: true, desktopNumber: 2, desktopLabel: 'M2', workbenchId: 'PrtCfg', ); /// 🧱 Dökümanı ve makrosu olan kart — bölümler açıldığında en YÜKSEK doğal /// boy. "Kart büyüyünce alanı aşar mı?" testi bunu kullanır. const _fullTile = PaletteTileView( id: 'CATIA-02', title: 'CATIA-02', fileName: 'Fuselage.CATPart', status: CatiaStatus.idle, pid: 4243, connected: true, desktopNumber: 1, desktopLabel: 'M1', docs: [ PaletteTileDocView(id: 'd1', name: 'Fuselage.CATPart', isActive: true), PaletteTileDocView(id: 'd2', name: 'Aileron.CATPart'), ], macros: [ PaletteMacroView(id: 'm1', label: 'Kaydet', emoji: '💾'), PaletteMacroView(id: 'm2', label: 'Ölçü', isCommand: true), ], ); /// 🧰 Komut ızgarasının çizilmesi için gereken ASGARİ öğeler. Boş olsaydı /// ekran `_Empty` çizer ve `GridView` hiç oluşmazdı (test ölçemezdi). final _entries = [ for (var i = 1; i <= 8; i++) PaletteEntryView( id: 'e$i', label: 'Komut $i', emoji: '🔧', tooltip: 'Komut $i', ), ]; /// Kartı [PaletteTileCards] içinde, [height] yüksekliğinde bir alana koyar — /// kart widget'ının KENDİ doğal boyunu ölçmek için (yerleşim kopyası değil, /// tek çocuk `PaletteTileCards`; kartın esnek olup olmadığı burada geçerli /// değildir). Widget _host(double height, {List tiles = const [_tile]}) => MaterialApp( theme: AppTheme.dark, home: Scaffold( body: SizedBox( height: height, child: Column( children: [ Flexible( fit: FlexFit.loose, child: PaletteTileCards(tiles: tiles, onAction: (_) {}), ), const Expanded(child: SizedBox.shrink()), ], ), ), ), ); void main() { testWidgets('HomeScreen açılır ve başlığı gösterir', (tester) async { await tester.pumpWidget( ChangeNotifierProvider( create: (_) => CatiaProvider(), child: MaterialApp(theme: AppTheme.dark, home: const HomeScreen()), ), ); // Provider açılışta native snapshot'ı 700 ms zaman aşımıyla sorgular ve // masaüstü yoklaması için zamanlayıcı kurar. Test sonunda "pending timer" // hatası olmaması için saati ilerletip ağacı boşaltıyoruz (provider // dispose edilince yoklama zamanlayıcısı iptal edilir). await tester.pump(const Duration(seconds: 1)); expect(find.text('FikoCATIA'), findsOneWidget); expect(find.textContaining('Aktif'), findsOneWidget); expect(find.textContaining('↕'), findsOneWidget); // Üst menü başlıkları (liste yerine bunlar kalıcı görünür). expect(find.text('Dosya'), findsOneWidget); expect(find.text('Görünüm'), findsOneWidget); expect(find.text('Araçlar'), findsOneWidget); expect(find.text('Yardım'), findsOneWidget); await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(const Duration(seconds: 5)); }); // ---- 🗂 Kart yerleşimi (regresyon: "kart yarı sayfayı kaplıyor") ---- group('🗂 kart yerleşimi', () { testWidgets('kart doğal boyunda kalır, boşluğu doldurmaz', (tester) async { await tester.pumpWidget(_host(220)); expect(find.byType(PaletteTileCard), findsOneWidget); // 220 px'lik alanda kart kendi doğal boyunda kalmalı. *tight* yerleşimde // (varsayılan FlexFit.tight) kart 220'nin tamamını kaplardı. final h = tester.getSize(find.byType(PaletteTileCard)).height; expect(h, lessThan(140), reason: 'kart $h px — boşluğu dolduruyor'); }); testWidgets('içerik büyüyünce alan aşılmaz, kaydırma devrede', ( tester, ) async { await tester.pumpWidget(_host(220, tiles: const [_fullTile])); expect(find.byType(PaletteTileCard), findsOneWidget); // Taşma (overflow) yasak: kart verilen yüksekliği aşamaz. expect( tester.getSize(find.byType(PaletteTileCard)).height, lessThanOrEqualTo(220), ); // Aşılırsa kullanıcı karta ulaşabilmeli → liste kaydırılabilir ve // kalan yüksekliği kendi içinde bırakır. expect( tester.widget(find.byType(ListView)).shrinkWrap, isTrue, reason: 'shrinkWrap olmadan liste tüm alanı yutar', ); }); testWidgets('düğmeler başlıkla aynı hizada (aşağı sarkık değil)', ( tester, ) async { await tester.pumpWidget(_host(260, tiles: const [_fullTile])); await tester.pumpAndSettle(); final titleRect = tester.getRect(find.text('CATIA-02')); // Düğme kutuları 16px, başlık 10px → *dikey ortalama* (eski hâl) farkı // yarım kutu kadar aşağı sarkıyordu. Üst kenarlar eşit olmalı. for (final label in ['👁', '✏️', '🔄', '🚪']) { final r = tester.getRect(find.text(label)); expect( r.top, lessThanOrEqualTo(titleRect.top + 1), reason: '$label başlıktan aşağı sarkık (üste dayalı değil)', ); } }); }); // ---- 🧰 Komut alanı (regresyon: "komutlar kalan kısmı kaplamıyor") ---- group('🧰 komut alanı', () { /// ⚠ Bu test BİLEREK GERÇEK `PaletteScreen`'i çalıştırır — düzeni kopyalamaz. /// Yerleşimin kendi kopyasını kullanan bir test, `palette_screen.dart`teki /// `flex: 0` silinse bile geçerdi (kendine özgü bir düzeni doğrulardı); /// yani asıl hatayı yakalayamazdı. Veri, alt pencerenin gerçek girdisi olan /// `PaletteChildLink.data` üzerinden verilir. Future pumpPalette( WidgetTester tester, { required double height, List tiles = const [_tile], bool cards = true, }) async { PaletteChildLink.data.value = PaletteStatePayload( cards: cards, tiles: tiles, targetTitle: 'CATIA', workbenchId: 'prtcfg', entries: _entries, ); addTearDown(() => PaletteChildLink.data.value = null); // ⚠ `MediaQuery` YETMEZ: yalnız bilgi taşır, `Scaffold`'un alacağı // kısıtı değiştirmez (test yüzeyi 800×600'de açılır). Gerçek boyutu // `SizedBox` belirler; `PaletteScreen` bu kutuya tam oturur. await tester.pumpWidget( MaterialApp( theme: AppTheme.dark, home: Center( child: SizedBox( width: 320, height: height, child: const PaletteScreen(), ), ), ), ); await tester.pumpAndSettle(); } /// Asıl yerleşimin **iç gövde `Column`'ını** bulur: kartı (`ListView`) ve /// komut ızgarasını (`GridView`) bir arada tutan Column. /// /// ⚠ `Stack` → `Container(border)` → `Column` zinciri **iki** Column /// içerir ve ikisi de ızgara torunlu görünür. Dış olan `SizedBox`'ın TAM /// yüksekliğini alır (220 − 2px çerçeve = 218) ve içinde 20px başlık şeridi /// ile 10px dolgu da barındırır → kart + ızgara toplamıyla **kıyaslanamaz** /// (30px'lik sahte fark). Bu yüzden en **derin** eşleşen Column seçilir: /// o, `Expanded(Padding(Column([Flexible(kart), SizedBox(4), Expanded(ızgara)])))` /// zincirinin ta kendisidir (188px). Finder bodyColumn(WidgetTester tester) { final cols = find.byType(Column).evaluate().toList(); bool hasBody(Finder c) => find .descendant(of: c, matching: find.byType(ListView)) .evaluate() .isNotEmpty || find .descendant(of: c, matching: find.byType(GridView)) .evaluate() .isNotEmpty; for (var i = cols.length - 1; i >= 0; i--) { if (hasBody(find.byElementPredicate((e) => e == cols[i]))) { return find.byElementPredicate((e) => e == cols[i]); } } throw StateError('kart/ızgara gövdesi bulunamadı'); } testWidgets('kalan yüksekliğin tamamını kaplar, boşluk bırakmaz', ( tester, ) async { await pumpPalette(tester, height: 220); // ⚠ `Flexible`ın `flex` değeri varsayılan **1**'dir. Kart esnek bırakılırsa // ızgara ile kalan yükseklik 50/50 BÖLÜŞÜLÜR ve kart kendi doğal // boyunda (≈35px) kalıp payının gerisini boşa bırakır → ızgara // (H-4)/2'ye düşer, alt yarısı boş kalır. Kart doğal boyda olduğuna göre // ızgara ALANIN TAMAMINI almalı. final body = tester.getSize(bodyColumn(tester)).height; final card = tester.getSize(find.byType(PaletteTileCard)).height; final grid = tester.getSize(find.byType(GridView)).height; expect( card + 4 + grid, closeTo(body, 0.5), reason: 'gövde $body, ama kart $card + 4 + ızgara $grid = ' '${card + 4 + grid} → ${body - (card + 4 + grid)}px boşluk var ' '(ızgara kalan yüksekliği doldurmuyor)', ); // 50/50 bölüşüm ≈ 94'e düşürürdü; ızgara gövdenin büyük çoğunluğunu // almalı (kart tek satır, ızgara kalan her şey). expect( grid, greaterThan(body * 0.7), reason: 'ızgara gövdenin çoğunu kaplamalı, kart ezmemeli', ); }); testWidgets('kart büyüdüğünde ızgara yine kalanın tamamını alır', ( tester, ) async { await pumpPalette(tester, height: 220, tiles: const [_fullTile]); final body = tester.getSize(bodyColumn(tester)).height; final card = tester.getSize(find.byType(PaletteTileCard)).height; final grid = tester.getSize(find.byType(GridView)).height; expect( card + 4 + grid, closeTo(body, 0.5), reason: 'döküman/makro bölümleri açıkken de boşluk kalmamalı', ); }); }); }