feat: patrol on-device integration smoke test
This commit is contained in:
parent
d6f4c434fa
commit
6f6fa20b58
10 changed files with 466 additions and 6 deletions
97
integration_test/smoke_test.dart
Normal file
97
integration_test/smoke_test.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// 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();
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue