[{"data":1,"prerenderedAt":79},["ShallowReactive",2],{"tutorial-golden-test-e-regressione-visiva-in-flutter-pixel-perfect-in-ci":3,"comments-tutorial-golden-test-e-regressione-visiva-in-flutter-pixel-perfect-in-ci":78},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":34},102,"Golden test e regressione visiva in Flutter: pixel-perfect in CI","golden-test-e-regressione-visiva-in-flutter-pixel-perfect-in-ci","Guida avanzata ai golden test in Flutter: font reali, dispositivi multipli, tolleranza sui pixel e integrazione in CI con confronto delle immagini di riferimento.","I widget test verificano la logica della UI, ma non il suo **aspetto**. I *golden test* (o snapshot test) rendono un widget in un'immagine PNG e la confrontano con un file di riferimento versionato nel repository: qualsiasi variazione di spaziatura, colore o tipografia fa fallire la build.\n\nIn questo tutorial avanzato costruiremo un'infrastruttura di regressione visiva robusta:\n\n- caricamento dei font reali per evitare i rettangoli neri di Ahem;\n- un helper per rendere lo stesso widget su più dimensioni di schermo e temi;\n- gestione della tolleranza di confronto per le differenze di rendering tra macchine;\n- esecuzione in CI con artefatti di diff scaricabili.\n\nSi presuppone familiarità con `flutter_test`, `pumpWidget` e la struttura di un progetto Flutter modulare.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Ffc79c923-1b2b-4db0-aaf3-32d65cb780b8.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1740479724824-5df685a87e31?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAwNTE2MDN8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Kofi Abban Davis","https:\u002F\u002Funsplash.com\u002F@wings_davis","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-laptop-computer-sitting-on-top-of-a-desk-OonCUQ2Vrrw","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=OjGUrRb9nXY",{"youtube_id":17,"duration_seconds":18,"published_at":19},"OjGUrRb9nXY",146,"2026-09-22T09:35:18+00:00","advanced",45,"3.x","published","2026-09-22T04:33:23+00:00","Golden test in Flutter: regressione visiva in CI","Tutorial avanzato sui golden test in Flutter: font reali, multi-device, tolleranza sui pixel e verifica della regressione visiva in CI.",{"id":28,"name":29,"slug":30},3,"Best practice","best-practice",{"id":32,"name":33},1,"Flutter Bot",[35,43,50,56,63,70],{"id":36,"position":32,"title":37,"body":38,"code_snippet":39,"code_language":40,"expected_result":41,"demo_url":42,"video_url":42},681,"Preparare il progetto e un widget da testare","I golden test girano con `flutter_test`, già presente. Isoliamo il componente da fotografare in un widget puro, senza dipendenze da rete o plugin: è la regola d'oro della regressione visiva.\n\nCrea `lib\u002Fui\u002Fprice_card.dart`. Nota l'uso di `Theme.of(context)`: testeremo lo stesso widget in light e dark mode.","import 'package:flutter\u002Fmaterial.dart';\n\nclass PriceCard extends StatelessWidget {\n  const PriceCard({super.key, required this.title, required this.price, this.badge});\n\n  final String title;\n  final double price;\n  final String? badge;\n\n  @override\n  Widget build(BuildContext context) {\n    final scheme = Theme.of(context).colorScheme;\n    return Card(\n      child: Padding(\n        padding: const EdgeInsets.all(16),\n        child: Column(\n          crossAxisAlignment: CrossAxisAlignment.start,\n          mainAxisSize: MainAxisSize.min,\n          children: [\n            if (badge != null)\n              Container(\n                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),\n                decoration: BoxDecoration(\n                  color: scheme.primaryContainer,\n                  borderRadius: BorderRadius.circular(999),\n                ),\n                child: Text(badge!, style: Theme.of(context).textTheme.labelSmall),\n              ),\n            const SizedBox(height: 8),\n            Text(title, style: Theme.of(context).textTheme.titleMedium),\n            Text('€ ${price.toStringAsFixed(2)}',\n                style: Theme.of(context).textTheme.headlineSmall),\n          ],\n        ),\n      ),\n    );\n  }\n}","dart","Un widget deterministico, senza stato asincrono, pronto per essere renderizzato in test.",null,{"id":44,"position":45,"title":46,"body":47,"code_snippet":48,"code_language":40,"expected_result":49,"demo_url":42,"video_url":42},682,2,"Caricare i font reali con flutter_test_config.dart","Per default i test usano il font *Ahem*, che disegna blocchi neri. La soluzione è caricare i font dal bundle degli asset di test tramite un file `test\u002Fflutter_test_config.dart`, eseguito automaticamente prima di tutti i test.\n\nAggiungi al `pubspec.yaml` il font che usi in app (es. `assets\u002Ffonts\u002FInter-Regular.ttf`) e poi registralo nel `FontLoader`. Nello stesso file impostiamo anche la tolleranza globale di confronto.","\u002F\u002F test\u002Fflutter_test_config.dart\nimport 'dart:async';\nimport 'dart:io';\nimport 'package:flutter\u002Fservices.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\n\nFuture\u003Cvoid> testExecutable(FutureOr\u003Cvoid> Function() testMain) async {\n  TestWidgetsFlutterBinding.ensureInitialized();\n  await _loadFont('Inter', ['assets\u002Ffonts\u002FInter-Regular.ttf', 'assets\u002Ffonts\u002FInter-Bold.ttf']);\n\n  \u002F\u002F Tolleranza: differenze minime di anti-aliasing tra macchine non fanno fallire.\n  goldenFileComparator = TolerantGoldenComparator(\n    Uri.parse('${Directory.current.path}\u002Ftest\u002F'),\n    tolerance: 0.005, \u002F\u002F 0.5% di pixel diversi\n  );\n\n  await testMain();\n}\n\nFuture\u003Cvoid> _loadFont(String family, List\u003CString> paths) async {\n  final loader = FontLoader(family);\n  for (final p in paths) {\n    loader.addFont(File(p).readAsBytes().then((b) => ByteData.view(b.buffer)));\n  }\n  await loader.load();\n}","Il testo nei golden viene disegnato con il font reale dell'app invece dei blocchi Ahem.",{"id":51,"position":28,"title":52,"body":53,"code_snippet":54,"code_language":40,"expected_result":55,"demo_url":42,"video_url":42},683,"Implementare un comparatore con tolleranza","`LocalFileComparator` fallisce a fronte di un solo pixel diverso: troppo fragile quando i golden sono generati su macOS e verificati su Linux in CI. Estendiamolo per accettare una percentuale di differenza, continuando a scrivere il file di diff in `failures\u002F`.","\u002F\u002F test\u002Ftolerant_golden_comparator.dart\nimport 'dart:typed_data';\nimport 'package:flutter_test\u002Fflutter_test.dart';\n\nclass TolerantGoldenComparator extends LocalFileComparator {\n  TolerantGoldenComparator(Uri testFile, {this.tolerance = 0.005})\n      : super(testFile);\n\n  final double tolerance;\n\n  @override\n  Future\u003Cbool> compare(Uint8List imageBytes, Uri golden) async {\n    final result = await GoldenFileComparator.compareLists(\n      imageBytes,\n      await getGoldenBytes(golden),\n    );\n    if (result.passed || result.diffPercent \u003C= tolerance) return true;\n\n    final error = await generateFailureOutput(result, golden, basedir);\n    throw FlutterError(error);\n  }\n}","Le micro-differenze di rendering vengono ignorate, mentre una regressione reale produce un PNG di diff.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":40,"expected_result":62,"demo_url":42,"video_url":42},684,4,"Helper multi-device e multi-tema","Un buon golden test copre più viewport e più temi senza duplicare codice. Creiamo un helper che imposta `physicalSize`, `devicePixelRatio` e il tema, e genera un nome file parlante.\n\n`matchesGoldenFile` accetta un path relativo alla cartella del test: organizziamo i file in `test\u002Fgoldens\u002F`.","\u002F\u002F test\u002Fgolden_helper.dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\n\nclass Device {\n  const Device(this.name, this.size, [this.dpr = 1.0]);\n  final String name;\n  final Size size;\n  final double dpr;\n\n  static const phone = Device('phone', Size(390, 844));\n  static const tablet = Device('tablet', Size(834, 1112));\n}\n\nFuture\u003Cvoid> goldenMatrix(\n  WidgetTester tester, {\n  required String name,\n  required Widget child,\n  List\u003CDevice> devices = const [Device.phone, Device.tablet],\n  List\u003CThemeMode> modes = const [ThemeMode.light, ThemeMode.dark],\n}) async {\n  for (final device in devices) {\n    for (final mode in modes) {\n      tester.view\n        ..physicalSize = device.size * device.dpr\n        ..devicePixelRatio = device.dpr;\n      addTearDown(tester.view.reset);\n\n      await tester.pumpWidget(MaterialApp(\n        debugShowCheckedModeBanner: false,\n        theme: ThemeData(useMaterial3: true, fontFamily: 'Inter'),\n        darkTheme: ThemeData.dark(useMaterial3: true).copyWith(),\n        themeMode: mode,\n        home: Scaffold(body: Center(child: Padding(padding: const EdgeInsets.all(24), child: child))),\n      ));\n      await tester.pumpAndSettle();\n\n      final suffix = mode == ThemeMode.dark ? 'dark' : 'light';\n      await expectLater(\n        find.byType(MaterialApp),\n        matchesGoldenFile('goldens\u002F${name}_${device.name}_$suffix.png'),\n      );\n    }\n  }\n}","Una sola chiamata produce 4 golden (2 device × 2 temi) con nomi coerenti.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":40,"expected_result":69,"demo_url":42,"video_url":42},685,5,"Scrivere il test e generare i golden","Ora il test è dichiarativo. Usa `--update-goldens` per generare i riferimenti la prima volta e **committali** nel repository: sono la fonte di verità.\n\n```bash\nflutter test --update-goldens test\u002Fprice_card_golden_test.dart\nflutter test   # verifica\n```\n\nSe modifichi il padding della card e rilanci senza `--update-goldens`, il test fallisce e in `test\u002Ffailures\u002F` trovi `*_masterImage.png`, `*_testImage.png`, `*_isolatedDiff.png`.","\u002F\u002F test\u002Fprice_card_golden_test.dart\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:myapp\u002Fui\u002Fprice_card.dart';\nimport 'golden_helper.dart';\n\nvoid main() {\n  testWidgets('PriceCard - stato base', (tester) async {\n    await goldenMatrix(\n      tester,\n      name: 'price_card_base',\n      child: const PriceCard(title: 'Piano Pro', price: 19.9),\n    );\n  });\n\n  testWidgets('PriceCard - con badge e testo lungo', (tester) async {\n    await goldenMatrix(\n      tester,\n      name: 'price_card_badge',\n      child: const PriceCard(\n        title: 'Piano Business annuale con fatturazione elettronica',\n        price: 199,\n        badge: 'PIÙ SCELTO',\n      ),\n    );\n  });\n}","8 file PNG in test\u002Fgoldens\u002F e un fallimento immediato al minimo cambio visivo non intenzionale.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":76,"expected_result":77,"demo_url":42,"video_url":42},686,6,"Eseguire i golden test in CI e pubblicare i diff","I golden vanno verificati **sempre sullo stesso sistema operativo** con cui sono stati generati: fissa Linux in CI e genera i riferimenti con lo stesso ambiente (anche tramite un container). Carica la cartella `failures\u002F` come artefatto per ispezionare le differenze dalla pull request.\n\nConsiglio operativo: escludi i golden dal `flutter test` generico dei PR di sola logica usando un tag, e aggiungi in `dart_test.yaml` o via `--tags` per separare le suite.","# .github\u002Fworkflows\u002Fgoldens.yml\nname: Golden tests\non: [pull_request]\n\njobs:\n  goldens:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.0'\n          channel: stable\n      - run: flutter pub get\n      - run: flutter test --reporter expanded\n      - name: Upload diff\n        if: failure()\n        uses: actions\u002Fupload-artifact@v4\n        with:\n          name: golden-failures\n          path: '**\u002Ftest\u002Ffailures\u002F**'","yaml","La pipeline blocca le regressioni visive e allega le immagini di diff scaricabili dalla PR.",[],1792316929876]