tane/apps/app_seeds/test/screenshots/screenshot_support.dart

201 lines
7 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:commons_core/commons_core.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show FontLoader, rootBundle;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/app.dart' show materialLocaleFor;
import 'package:tane/data/species_repository.dart';
import 'package:tane/data/variety_repository.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import 'package:tane/domain/crop_calendar.dart' show monthsToMask;
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/state/inventory_cubit.dart';
import 'package:tane/ui/theme.dart';
/// Marketing screenshot canvas: a tall phone at 3x. Logical 360x760.
const screenshotSize = Size(1080, 2280);
const screenshotPixelRatio = 3.0;
/// Locales we render real, translated screenshots for. Asturian (`ast`) is
/// omitted from store assets; the RTL demo is handled separately (the app has
/// no Arabic strings yet — see [ScreenshotLocale.rtlDemo]).
const screenshotLocales = <AppLocale>[
AppLocale.en,
AppLocale.es,
AppLocale.fr,
AppLocale.de,
AppLocale.pt,
AppLocale.ja,
];
bool _fontsLoaded = false;
/// Registers real glyph-bearing fonts so golden screenshots show text and icons
/// rather than the Ahem/tofu boxes flutter_test uses by default. Loads every
/// family declared in the test bundle's FontManifest.json (MaterialIcons, the
/// bundled Noto Sans JP/Arabic and seedks glyphs), then adds DejaVu Sans — a
/// bundled asset, not a font *family* — as the Latin text face so en/es/fr/de/pt
/// render legibly without needing Roboto.
Future<void> loadScreenshotFonts() async {
if (_fontsLoaded) return;
Future<void> family(String name, List<String> assets) async {
final loader = FontLoader(name);
for (final asset in assets) {
loader.addFont(rootBundle.load(asset));
}
await loader.load();
}
final manifest =
json.decode(await rootBundle.loadString('FontManifest.json'))
as List<dynamic>;
for (final entry in manifest.cast<Map<String, dynamic>>()) {
await family(
entry['family'] as String,
[for (final f in entry['fonts'] as List) f['asset'] as String],
);
}
await family('DejaVu Sans', [
'assets/fonts/DejaVuSans.ttf',
'assets/fonts/DejaVuSans-Bold.ttf',
]);
_fontsLoaded = true;
}
const _fontFallbacks = <String>['Noto Sans JP', 'Noto Sans Arabic'];
/// The real app theme, but with every text style pinned to the bundled
/// [DejaVu Sans] so screenshots render legible glyphs (see [loadScreenshotFonts]).
ThemeData screenshotTheme() {
final base = buildTaneTheme();
return base.copyWith(
textTheme: base.textTheme.apply(
fontFamily: 'DejaVu Sans',
fontFamilyFallback: _fontFallbacks,
),
primaryTextTheme: base.primaryTextTheme.apply(
fontFamily: 'DejaVu Sans',
fontFamilyFallback: _fontFallbacks,
),
appBarTheme: base.appBarTheme.copyWith(
titleTextStyle: base.appBarTheme.titleTextStyle?.copyWith(
fontFamily: 'DejaVu Sans',
fontFamilyFallback: _fontFallbacks,
),
),
);
}
/// Wraps [child] as a full screen for capture: the real green theme with
/// screenshot fonts, the given [locale], repositories and an [InventoryCubit].
/// Pass [textDirection] to force RTL for the layout demo.
Widget screenshotApp({
required VarietyRepository repository,
required SpeciesRepository species,
required Widget child,
AppLocale locale = AppLocale.en,
TextDirection? textDirection,
}) {
LocaleSettings.setLocaleSync(locale);
return TranslationProvider(
child: MultiRepositoryProvider(
providers: [
RepositoryProvider.value(value: repository),
RepositoryProvider.value(value: species),
],
child: BlocProvider(
create: (_) => InventoryCubit(repository),
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: screenshotTheme(),
locale: materialLocaleFor(locale.flutterLocale),
supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: textDirection == null
? child
: Directionality(textDirection: textDirection, child: child),
),
),
),
);
}
/// One seeded variety plus the id of a "hero" variety used for the detail shot.
class SeededInventory {
const SeededInventory(this.heroVarietyId);
final String heroVarietyId;
}
/// Seeds an attractive, internationally-neutral inventory: a spread of
/// colour-coded botanical families, lots with years/quantities/origins, photo
/// avatars, and crop-calendar month masks so the calendar screen is populated.
/// Returns the hero variety (a cherry tomato with several lots) for detail.
Future<SeededInventory> seedShowcase(
AppDatabase db,
VarietyRepository repo,
) async {
// Sow in spring, harvest seed in late summer — visible in the calendar.
final sow = monthsToMask(const [3, 4, 5]);
final harvest = monthsToMask(const [8, 9]);
// Most varieties use the app's default coloured-initial disc avatar; the hero
// (maize) carries a real photo (below).
Future<String> add(String label, String family, {Uint8List? photo}) async {
final id = await repo.addQuickVariety(
label: label,
category: family,
photoBytes: photo,
);
await (db.update(db.varieties)..where((v) => v.id.equals(id))).write(
VarietiesCompanion(
sowMonths: Value(sow),
seedHarvestMonths: Value(harvest),
),
);
return id;
}
// A real photo of red & white flint corn is the hero variety, so the detail
// shot shows a genuine seed photo. Loaded from the committed test fixture
// (CWD is the package root under `flutter test`).
final maizePhoto = await File(
'test/screenshots/fixtures/maize.jpg',
).readAsBytes();
final maize = await add('Painted flint corn', 'Poaceae', photo: maizePhoto);
await repo.addLot(
varietyId: maize,
harvestYear: 2024,
quantity: const Quantity(kind: QuantityKind.cob, count: 6),
originName: 'Community seed swap',
abundance: Abundance.plentyToShare,
offerStatus: OfferStatus.shared,
);
await repo.addLot(
varietyId: maize,
harvestYear: 2023,
quantity: const Quantity(kind: QuantityKind.packet, count: 1),
offerStatus: OfferStatus.exchange,
);
// The rest sit in families that sort at/after Poaceae, so the maize (and its
// photo) stays the first, top-of-list group in the inventory shot.
await add('Common buckwheat', 'Polygonaceae');
await add('Alpine strawberry', 'Rosaceae');
await add('Cherry tomato', 'Solanaceae');
await add('Sweet pepper', 'Solanaceae');
return SeededInventory(maize);
}