From 6f6fa20b583000d8076091f3b860b0a084a83a76 Mon Sep 17 00:00:00 2001 From: randogoth Date: Mon, 28 Sep 2026 23:49:39 +0300 Subject: [PATCH] feat: patrol on-device integration smoke test --- .gitignore | 11 ++ android/.gitignore | 5 +- android/app/build.gradle.kts | 26 +++ .../integration/MainActivityTest.java | 38 ++++ android/gradlew | 171 ++++++++++++++++++ devbox.json | 13 +- devbox.lock | 8 + integration_test/smoke_test.dart | 97 ++++++++++ pubspec.lock | 89 ++++++++- pubspec.yaml | 14 ++ 10 files changed, 466 insertions(+), 6 deletions(-) create mode 100644 android/app/src/androidTest/java/com/app/smol_mail/integration/MainActivityTest.java create mode 100755 android/gradlew create mode 100644 integration_test/smoke_test.dart diff --git a/.gitignore b/.gitignore index 24476c5..a305876 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,14 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/build + +# Rust wrapper crate (native/) +native/target/ + +# Rust cdylibs built into the APK by devbox run build-native-android +android/app/src/main/jniLibs/ + +# Patrol generates the bundle from the integration tests at run time +**/test_bundle.dart +.patrol.env diff --git a/android/.gitignore b/android/.gitignore index 6f56801..1e367f8 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -1,8 +1,9 @@ gradle-wrapper.jar /.gradle /captures/ -/gradlew -/gradlew.bat +# gradlew stays tracked: it carries the nix project-cache redirect that every +# non-flutter gradle invocation (patrol_cli, manual builds) needs, so the +# wrapper can't be re-injected from the template. /local.properties GeneratedPluginRegistrant.java diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index db7cfc9..22ada6c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -25,6 +25,25 @@ android { targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + // The Android Test Orchestrator (with clearPackageData) wipes the + // integration flavor's app data between test runs, and the flavor + // installs it beside — never over — the real app, so every patrol run + // starts at onboarding with a fresh identity. + testInstrumentationRunner = "pl.leancode.patrol.PatrolJUnitRunner" + testInstrumentationRunnerArguments["clearPackageData"] = "true" + } + + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } + + flavorDimensions += "test" + productFlavors { + create("integration") { + dimension = "test" + applicationIdSuffix = ".integration" + versionNameSuffix = "-integration" + } } buildTypes { @@ -38,3 +57,10 @@ android { flutter { source = "../.." } + +dependencies { + // The orchestrator referenced by testOptions.execution above. The patrol + // native library itself comes from the patrol pub package's android + // module (a plugin subproject), not from a Maven artifact. + androidTestUtil("androidx.test:orchestrator:1.5.1") +} diff --git a/android/app/src/androidTest/java/com/app/smol_mail/integration/MainActivityTest.java b/android/app/src/androidTest/java/com/app/smol_mail/integration/MainActivityTest.java new file mode 100644 index 0000000..cf41662 --- /dev/null +++ b/android/app/src/androidTest/java/com/app/smol_mail/integration/MainActivityTest.java @@ -0,0 +1,38 @@ +package com.app.smol_mail.integration; + +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import pl.leancode.patrol.PatrolJUnitRunner; + +// The Patrol host class: each Dart test in the integration_test directory is +// executed through this JUnit suite. Its package is the flavored applicationId +// (com.app.smol_mail + .integration suffix); the namespace differs, so +// MainActivity needs the explicit import below. +@RunWith(Parameterized.class) +public class MainActivityTest { + + @Parameters(name = "{0}") + public static Object[] testCases() { + PatrolJUnitRunner instrumentation = + (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + instrumentation.setUp(com.example.smol_mail.MainActivity.class); + instrumentation.waitForPatrolAppService(); + return instrumentation.listDartTests(); + } + + public MainActivityTest(String dartTestName) { + this.dartTestName = dartTestName; + } + + private final String dartTestName; + + @Test + public void runDartTest() { + PatrolJUnitRunner instrumentation = + (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + instrumentation.runDartTest(dartTestName); + } +} diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..a32d0e0 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,171 @@ +#!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +# Nix's flutter keeps the flutter_tools/gradle composite build on a read-only +# store path, so every gradle invocation needs its project and Kotlin caches +# redirected off it — the same two arguments the flutter tool itself passes. +# Without them, any gradlew run other than through the flutter tool (patrol_cli, +# manual builds) fails with a silent exit 1. +_nix_cache="${XDG_CACHE_HOME:-$HOME/.cache}/flutter/nix-flutter-tools-gradle/gradle-project-cache" +case " $* " in + *" --project-cache-dir"*) ;; + *) set -- "$@" "--project-cache-dir=$_nix_cache" "-Pkotlin.project.persistent.dir=$_nix_cache/kotlin" ;; +esac + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/devbox.json b/devbox.json index 2c74757..d8a38b7 100644 --- a/devbox.json +++ b/devbox.json @@ -10,7 +10,9 @@ "nixpkgs#ninja", "nixpkgs#pkg-config", "nixpkgs#gtk3", - "nixpkgs#libsysprof-capture" + "nixpkgs#libsysprof-capture", + "nixpkgs#rustup", + "nixpkgs#cargo-ndk" ], "shell": { "init_hook": [ @@ -22,11 +24,16 @@ "test": "flutter test", "run-linux": "flutter run -d linux", "build-linux": "flutter build linux", - "link-android-tools": "if [ -n \"$ANDROID_HOME\" ] && [ ! -d \"$ANDROID_HOME/cmdline-tools/latest\" ] && [ -d \"$ANDROID_HOME/cmdline-tools/8.0\" ]; then ln -s \"$ANDROID_HOME/cmdline-tools/8.0\" \"$ANDROID_HOME/cmdline-tools/latest\"; fi" + "link-android-tools": "if [ -n \"$ANDROID_HOME\" ] && [ ! -d \"$ANDROID_HOME/cmdline-tools/latest\" ] && [ -d \"$ANDROID_HOME/cmdline-tools/8.0\" ]; then ln -s \"$ANDROID_HOME/cmdline-tools/8.0\" \"$ANDROID_HOME/cmdline-tools/latest\"; fi", + "build-native": "cd native && cargo build --release", + "build-native-android": "rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android && cd native && cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -o ../android/app/src/main/jniLibs build --release", + "test-native": "cd native && cargo test --release && cargo test --release -- --ignored", + "activate-patrol": "dart pub global activate patrol_cli", + "test-integration": "export PATH=\"$HOME/.pub-cache/bin:$PATH\" && patrol test --dart-define=SMOL_TEST_HOST=$(hostname -I | tr ' ' '\\n' | grep -E '^(192\\.168|10\\.|172\\.)' | head -1)" } }, "env": { "ANDROID_SDK_ROOT": "$DEVBOX_PACKAGES_DIR/share/android-sdk", "ANDROID_HOME": "$DEVBOX_PACKAGES_DIR/share/android-sdk" } -} \ No newline at end of file +} diff --git a/devbox.lock b/devbox.lock index 6ffe55e..5db83f3 100644 --- a/devbox.lock +++ b/devbox.lock @@ -57,6 +57,10 @@ "last_modified": "1970-01-01T00:00:00Z", "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#android-tools" }, + "nixpkgs#cargo-ndk": { + "last_modified": "1970-01-01T00:00:00Z", + "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#cargo-ndk" + }, "nixpkgs#clang": { "last_modified": "1970-01-01T00:00:00Z", "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#clang" @@ -84,6 +88,10 @@ "nixpkgs#pkg-config": { "last_modified": "1970-01-01T00:00:00Z", "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#pkg-config" + }, + "nixpkgs#rustup": { + "last_modified": "1970-01-01T00:00:00Z", + "resolved": "path:/nix/store/v8ynlbrldn7k8byb1kx0j0wka8dsbrw9-source?narHash=sha256-BZWCi9ZRJiARTuKTbbtvFTj7t1TK4G3UEckT3HyNfRg%3D#rustup" } } } diff --git a/integration_test/smoke_test.dart b/integration_test/smoke_test.dart new file mode 100644 index 0000000..7a6c75a --- /dev/null +++ b/integration_test/smoke_test.dart @@ -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 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(); + }); +} diff --git a/pubspec.lock b/pubspec.lock index a96a269..b7c28cf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,6 +249,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.8.0" + dispose_scope: + dependency: transitive + description: + name: dispose_scope + sha256: "48ec38ca2631c53c4f8fa96b294c801e55c335db5e3fb9f82cede150cfe5a2af" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" + url: "https://pub.dev" + source: hosted + version: "2.1.0" fake_async: dependency: transitive description: @@ -258,7 +274,7 @@ packages: source: hosted version: "1.3.3" ffi: - dependency: transitive + dependency: "direct main" description: name: ffi sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" @@ -334,6 +350,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -384,6 +405,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -440,6 +466,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.7" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -464,6 +498,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.10.1" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: "direct main" description: @@ -672,6 +711,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + patrol: + dependency: "direct dev" + description: + name: patrol + sha256: "662c50d517b2f159fedbc359a7fb8d0d30cbefea6d5c2a08392a0593079d64d1" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + patrol_finders: + dependency: transitive + description: + name: patrol_finders + sha256: "8608cdacaab90a3b00ecd7b09df11f13a62b01dea13c211b9f13fd86854e103a" + url: "https://pub.dev" + source: hosted + version: "3.6.0" + patrol_log: + dependency: transitive + description: + name: patrol_log + sha256: "3d91c93e8d0ed19cb12d4b27aa24eca7294e0a48d9d8445505c9775feb5fde2d" + url: "https://pub.dev" + source: hosted + version: "0.10.1" petitparser: dependency: transitive description: @@ -712,6 +775,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.2" + process: + dependency: transitive + description: + name: process + sha256: "4242ba3508d37e01808bdf71ad1d5bb93a8d671bf2e7450e6b1b353fb0808891" + url: "https://pub.dev" + source: hosted + version: "5.0.6" pub_semver: dependency: transitive description: @@ -869,6 +940,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -1005,6 +1084,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "28b82ec894fed45dd71c23ba62d1af973ed97dd59a4f5790a4d38b0b13e5657e" + url: "https://pub.dev" + source: hosted + version: "3.2.0" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 52af655..9738da7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: flutter: sdk: flutter crypto: ^3.0.3 + ffi: ^2.1.0 hive: ^2.2.3 hive_flutter: ^1.1.0 auto_route: ">=11.1.0 <11.2.0" @@ -24,12 +25,25 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter + patrol: ^4.10.0 build_runner: ^2.4.6 flutter_lints: ^6.0.0 auto_route_generator: ^10.5.0 flutter_native_splash: ^2.3.4 flutter_launcher_icons: ^0.14.4 +# patrol test reads this block; android.package_name is the flavored +# applicationId (the integration flavor adds the .integration suffix), and +# patrol_cli refuses to run without it. +patrol: + app_name: kirakira + flavor: integration + test_directory: integration_test + android: + package_name: com.app.smol_mail.integration + flutter_launcher_icons: android: "launcher_icon" ios: false