kirakira/integration_test/smoke_test.dart
2026-09-29 18:19:50 +03:00

97 lines
4.1 KiB
Dart

// 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<Widget> 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 <unreadable>.
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();
});
}