// Device integration tests over the real UI, driven by Patrol against the // system bunshin on the test host. The integration flavor installs beside // the real app and the instrumentation runner clears its data, so every run // starts at onboarding with a fresh identity — the app's own flows are the // subject under test, including the network path. // // Run: devbox run test-integration // (the script resolves the host's LAN IP into SMOL_TEST_HOST, because the // device reaches the server over Wi-Fi, not loopback.) import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:hive_flutter/hive_flutter.dart"; import "package:path_provider/path_provider.dart"; import "package:patrol/patrol.dart"; import "package:smol_mail/data/providers/providers.dart"; import "package:smol_mail/presentation/app_widget.dart"; import "package:smol_mail/smol/config.dart"; import "package:smol_mail/smol/store.dart"; const testHost = String.fromEnvironment("SMOL_TEST_HOST", defaultValue: "10.0.2.2"); // The system bunshin's static key: a documented, operator-supplied value on // this machine — the trusted channel SPEC.md §4 asks a pin to come from. const serverKey = "lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq"; Future bootApp() async { // main()'s boot, minus runApp: the test pumps the same tree. final dataDir = await getApplicationSupportDirectory(); Hive.init(dataDir.path); final store = await SmolStore.open(dbPath: "${dataDir.path}/smol-mail.db"); applyPresetServer(store); return ProviderScope( overrides: [storeProvider.overrideWithValue(store)], child: const AppWidget(), ); } void main() { patrolTest("smoke: onboarding, register, self-send, fetch, read in requests", ($) async { await $.pumpWidgetAndSettle(await bootApp()); // Onboarding: a fresh identity, backed up, registered under a name no // server has bound yet — registration must succeed against the real // server over Wi-Fi, through the Dart-resolved dial path. await $("Create Identity").tap(); await $.pumpAndSettle(); await $("I have backed it up").waitUntilVisible(); await $("I have backed it up").tap(); await $.pumpAndSettle(); final user = "it${DateTime.now().millisecondsSinceEpoch.toRadixString(36)}"; // Enter text by TextField, not by its labelText: the decoration label is // a RichText that is not hit-testable, so a text finder times out. await $.enterText($(TextField).at(0), "$user@$testHost:1961"); await $.enterText($(TextField).at(1), serverKey); await $("Pin and Register").tap(); // The register round trip is real network: the inbox tabs only exist // behind HomeGuard, so reaching them proves the account bound. await $("Inbox").waitUntilVisible(timeout: const Duration(seconds: 30)); await $("Requests").waitUntilVisible(); // Compose the first self-addressed mail. await $(Icons.edit).tap(); await $.pumpAndSettle(); await $.enterText($(TextField).at(0), "$user@$testHost:1961"); await $.enterText($(TextField).at(1), "smoke subject"); await $.enterText($(TextField).at(2), "smoke body from patrol"); await $("Send").tap(); // Compose pops back to the inbox once the send round trip completes. await $("Inbox").waitUntilVisible(timeout: const Duration(seconds: 30)); await $.pumpAndSettle(); // Fetch over the air, then read what arrived. await $(Icons.cloud_download).tap(); await $.pumpAndSettle(duration: const Duration(seconds: 5)); await $("Requests (1)").tap(); await $.pumpAndSettle(); // First contact is unsolicited, so it landed in requests (sec 5.8) — // and the §5.6 sent copy must read back as plain text, not . await $("smoke subject").tap(); await $.pumpAndSettle(); await $("smoke body from patrol").waitUntilVisible(); await $.native.pressBack(); // ignore: deprecated_member_use await $.pumpAndSettle(); await $("Sent").tap(); await $.pumpAndSettle(); await $("smoke subject").tap(); await $.pumpAndSettle(); await $("smoke body from patrol").waitUntilVisible(); }); }