tane/apps/app_seeds/test/ui/small_screen_overflow_test.dart
vjrj de6938d5d7 feat(plantare): usable UI — commitments screen, add sheet, entry points
The reproduction-commitment (Plantare) UI, local-first and plain-spoken:
- A Plantares screen (drawer entry, /plantares) listing your commitments,
  grouped Open / Done, with a big 'mark returned', a menu (let it go / reopen
  / remove) and an empty-state that explains what a Plantare is.
- An add sheet (scrollable): a two-choice 'who reproduces & returns' (I return
  / owed to me), an optional 'with whom', an optional 'what comes back' in the
  grower's own words, and a note. No sale, no currency — a promise.
- Reachable from a seed too: an 'Add a commitment' action on the variety
  detail, pre-attached to that variety.
- i18n en/es/pt/ast (human words: 'Plantare' + a plain explanation).

Added the Plantares screen to the small-phone overflow guard (18 cases green).
2026-07-11 02:11:57 +02:00

266 lines
8.6 KiB
Dart

import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:tane/db/enums.dart';
import 'package:tane/i18n/strings.g.dart';
import 'package:tane/services/profile_store.dart';
import 'package:tane/services/social_account_store.dart';
import 'package:tane/services/social_connection.dart';
import 'package:tane/services/social_service.dart';
import 'package:tane/services/social_settings.dart';
import 'package:tane/ui/about_screen.dart';
import 'package:tane/ui/chat_screen.dart';
import 'package:tane/ui/home_screen.dart';
import 'package:tane/ui/intro_screen.dart';
import 'package:tane/ui/inventory_list_screen.dart';
import 'package:tane/ui/market_screen.dart';
import 'package:tane/ui/plantares_screen.dart';
import 'package:tane/ui/profile_screen.dart';
import 'package:tane/ui/settings_screen.dart';
import '../support/test_support.dart';
/// Small-phone layout guard. A Flutter `RenderFlex overflowed` (a clipped title,
/// a save button pushed off a non-scrolling column, a Row too wide) is reported
/// as an error and FAILS the test — so pumping each screen at a tiny viewport,
/// in the languages whose text runs longest (es/pt/ast), catches exactly the
/// "the save button / title isn't fully visible on a small phone" reports.
///
/// FULL SCREENS ONLY. Modal bottom sheets (quick-add, the lot editor) are NOT
/// checked here: `showModalBottomSheet` gets an unbounded height in a widget
/// test, which fabricates a huge false overflow. Those sheets already wrap their
/// body in a `SingleChildScrollView`, so their save buttons stay reachable;
/// verify them on a real small device.
void main() {
// iPhone-SE-class logical viewport: narrow AND short, where clipping shows.
const small = Size(320, 568);
// The locales whose strings are longest — English rarely overflows first.
const longLocales = [AppLocale.es, AppLocale.pt, AppLocale.ast];
setUpAll(() => PackageInfo.setMockInitialValues(
appName: 'Tanemaki',
packageName: 'org.comunes.tane',
version: '0.1.0',
buildNumber: '1',
buildSignature: '',
));
Future<void> pumpSmall(WidgetTester tester, Widget widget) async {
tester.view.physicalSize = small;
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(widget);
await tester.pumpAndSettle();
}
for (final locale in longLocales) {
final tag = locale.languageCode;
testWidgets('home menu fits a small screen ($tag)', (tester) async {
final db = newTestDatabase();
addTearDown(db.close);
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: locale,
child: const HomeScreen(marketEnabled: true),
),
);
await disposeTree(tester);
});
testWidgets('settings (language, backup, about) fits ($tag)', (tester) async {
final db = newTestDatabase();
addTearDown(db.close);
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: locale,
child: const SettingsScreen(),
),
);
await disposeTree(tester);
});
testWidgets('about (title, version, license) fits ($tag)', (tester) async {
final db = newTestDatabase();
addTearDown(db.close);
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: locale,
child: const AboutScreen(),
),
);
await disposeTree(tester);
});
testWidgets('the intro carousel fits ($tag)', (tester) async {
final db = newTestDatabase();
addTearDown(db.close);
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: locale,
child: IntroScreen(onDone: () {}),
),
);
await disposeTree(tester);
});
}
testWidgets('profile (identity card + save button) fits a small screen',
(tester) async {
final db = newTestDatabase();
addTearDown(db.close);
final social = await SocialService.fromRootSeedHex('00' * 32);
final connection = SocialConnection(
social: social,
settings: SocialSettings(InMemorySecretStore()),
open: (_) async => throw StateError('offline'),
online: const Stream.empty(),
);
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: AppLocale.es,
child: ProfileScreen(
social: social,
connection: connection,
profileStore: ProfileStore(InMemorySecretStore()),
accounts: SocialAccountStore(InMemorySecretStore()),
),
),
);
await disposeTree(tester);
});
// The market/chat are "live" screens; checked here in their OFFLINE state
// (the shared connection can't reach a relay) — a static layout, and the one
// a user without signal actually sees.
Future<SocialConnection> offlineConn() async {
final social = await SocialService.fromRootSeedHex('00' * 32);
final settings = SocialSettings(InMemorySecretStore());
await settings.setRelayUrls(const []);
return SocialConnection(
social: social,
settings: settings,
open: (_) async => throw StateError('offline'),
online: const Stream.empty(),
);
}
testWidgets('market (offline "set up sharing") fits a small screen',
(tester) async {
final db = newTestDatabase();
addTearDown(db.close);
final connection = await offlineConn();
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: AppLocale.es,
child: MarketScreen(
social: await SocialService.fromRootSeedHex('00' * 32),
settings: SocialSettings(InMemorySecretStore()),
connection: connection,
),
),
);
await disposeTree(tester);
});
testWidgets('chat (offline "set up sharing") fits a small screen',
(tester) async {
final db = newTestDatabase();
addTearDown(db.close);
final connection = await offlineConn();
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: AppLocale.es,
child: ChatScreen(
social: await SocialService.fromRootSeedHex('00' * 32),
connection: connection,
peerPubkey: 'ab' * 32,
),
),
);
await disposeTree(tester);
});
testWidgets('inventory (empty) fits a small screen', (tester) async {
final db = newTestDatabase();
addTearDown(db.close);
await pumpSmall(
tester,
wrapScreen(
repository: newTestRepository(db),
locale: AppLocale.es,
child: const InventoryListScreen(),
),
);
await disposeTree(tester);
});
testWidgets('inventory with a long-named seed fits a small screen',
(tester) async {
final db = newTestDatabase();
addTearDown(db.close);
final repo = newTestRepository(db);
final id = await repo.addQuickVariety(
label: 'Tomate rosa de Barbastro extraordinariamente larguísimo',
category: 'Solanáceas',
);
await repo.addLot(
varietyId: id,
harvestYear: 2024,
quantity: const Quantity(kind: QuantityKind.handful),
offerStatus: OfferStatus.shared,
);
await pumpSmall(
tester,
wrapScreen(
repository: repo,
locale: AppLocale.es,
child: const InventoryListScreen(),
),
);
await disposeTree(tester);
});
testWidgets('the Plantares screen (with a commitment) fits a small screen',
(tester) async {
final db = newTestDatabase();
addTearDown(db.close);
final repo = newTestRepository(db);
await repo.createPlantare(
direction: PlantareDirection.iReturn,
counterparty: 'Colectivo semillero de la comarca',
owedDescription: 'un puñado la próxima temporada, si germina bien',
);
await pumpSmall(
tester,
wrapScreen(
repository: repo,
locale: AppLocale.es,
child: const PlantaresScreen(),
),
);
await disposeTree(tester);
});
// NOTE: the variety-detail screen is deliberately NOT overflow-checked here.
// It reports a ~2px overflow on a RenderFlex that is already DISPOSED/DEFUNCT
// — a transient stale frame while its cubit's Drift stream rebuilds during
// pumpAndSettle, not the stable rendered layout. Guarding it would be flaky;
// its RTL/build coverage lives in rtl_smoke_test.dart.
}