98 lines
4.1 KiB
Dart
98 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();
|
||
|
|
});
|
||
|
|
}
|