90bb6598d4cb

test: emulator QA harness for autofill chips — vault seed + native form probe

VaultSeedTest seeds a throwaway vault (GitHub/Proton Mail/Vercel) in-process via a Hilt entry-point seam (VaultRepositoryEntryPoint) so chip testing never depends on IME text injection. QaFormActivity (Java — the androidTest APK ships no Kotlin runtime) + QaFormAutofillTest prove the framework issues fill requests for native forms and catch regressions of the inline-suggestion pipeline; both green on emulator-5554 (API 36).

time
2026-08-05 00:40 UTC · 2 months ago
machine
koh2
parent
4a5b20b003e3
5 files app/src/androidTest/AndroidManifest.xml+8 −0app/src/androidTest/java/com/peachpasswords/qa/QaFormActivity.java+40 −0app/src/androidTest/java/com/peachpasswords/qa/QaFormAutofillTest.kt+50 −0app/src/androidTest/java/com/peachpasswords/qa/VaultSeedTest.kt+69 −0app/src/main/java/com/peachpasswords/di/VaultModule.kt+9 −0
app/src/androidTest/AndroidManifest.xml+8 −0
@@ -0,0 +1,8 @@1+<?xml version="1.0" encoding="utf-8"?>2+<manifest xmlns:android="http://schemas.android.com/apk/res/android">3+    <application>4+        <activity5+            android:name="com.peachpasswords.qa.QaFormActivity"6+            android:exported="true" />7+    </application>8+</manifest>

view file

app/src/androidTest/java/com/peachpasswords/qa/QaFormActivity.java+40 −0
@@ -0,0 +1,40 @@1+package com.peachpasswords.qa;2+3+import android.app.Activity;4+import android.os.Bundle;5+import android.text.InputType;6+import android.view.View;7+import android.widget.EditText;8+import android.widget.LinearLayout;9+10+/**11+ * Minimal native login form for emulator autofill QA. Java on purpose: the12+ * androidTest APK ships without the Kotlin runtime, so a standalone13+ * `am start` launch of this activity must not need it. Lives in the test14+ * package: a different package from the Peach autofill service, so the15+ * framework will happily send fill requests for it.16+ */17+public class QaFormActivity extends Activity {18+    @Override19+    protected void onCreate(Bundle savedInstanceState) {20+        super.onCreate(savedInstanceState);21+        EditText username = new EditText(this);22+        username.setId(android.R.id.edit);23+        username.setHint("Username");24+        username.setAutofillHints("username");25+        username.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);26+27+        EditText password = new EditText(this);28+        password.setHint("Password");29+        password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);30+        password.setAutofillHints("password");31+        password.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_YES);32+33+        LinearLayout layout = new LinearLayout(this);34+        layout.setOrientation(LinearLayout.VERTICAL);35+        layout.setPadding(48, 96, 48, 0);36+        layout.addView(username);37+        layout.addView(password);38+        setContentView(layout);39+    }40+}

view file

app/src/androidTest/java/com/peachpasswords/qa/QaFormAutofillTest.kt+50 −0
@@ -0,0 +1,50 @@1+package com.peachpasswords.qa2+3+import androidx.test.ext.junit.runners.AndroidJUnit44+import androidx.test.platform.app.InstrumentationRegistry5+import java.io.BufferedReader6+import java.io.InputStreamReader7+import org.junit.Assert.assertTrue8+import org.junit.Test9+import org.junit.runner.RunWith10+11+/**12+ * Emulator-QA probe: does the autofill framework issue fill requests for a13+ * native form at all? Launches [QaFormActivity] in the TEST package (a14+ * different package from the Peach autofill service, so the framework will15+ * fill it), taps the username field, then reads `dumpsys autofill`16+ * (shell-priv via UiAutomation) and asserts a session mentions the test17+ * package. If this fails, autofill is blocked emulator-wide; if it passes,18+ * a Chrome-specific problem is confirmed.19+ */20+@RunWith(AndroidJUnit4::class)21+class QaFormAutofillTest {22+23+    @Test24+    fun frameworkIssuesFillRequestForNativeForm() {25+        shell("am start -n com.peachpasswords.debug.test/com.peachpasswords.qa.QaFormActivity")26+        Thread.sleep(3000)27+        // Username EditText: top of screen, full width (padding 48/96 in the activity).28+        shell("input tap 360 150")29+        Thread.sleep(4000)30+        val dump = shell("dumpsys autofill")31+        val mentions = dump.lineSequence()32+            .filter { it.contains("com.peachpasswords.debug.test") }33+            .take(10)34+            .joinToString("\n")35+        assertTrue(36+            "no autofill session for the QA form — framework never issued a fill request.\n" +37+                "dumpsys excerpt:\n$mentions",38+            mentions.isNotEmpty(),39+        )40+    }41+42+    private fun shell(command: String): String {43+        val instrumentation = InstrumentationRegistry.getInstrumentation()44+        instrumentation.uiAutomation.executeShellCommand(command).use { pfd ->45+            BufferedReader(InputStreamReader(android.os.ParcelFileDescriptor.AutoCloseInputStream(pfd))).use {46+                return it.readText()47+            }48+        }49+    }50+}

view file

app/src/androidTest/java/com/peachpasswords/qa/VaultSeedTest.kt+69 −0
@@ -0,0 +1,69 @@1+package com.peachpasswords.qa2+3+import android.content.Context4+import androidx.test.core.app.ApplicationProvider5+import androidx.test.ext.junit.runners.AndroidJUnit46+import com.peachpasswords.di.VaultRepositoryEntryPoint7+import com.peachpasswords.vault.CreateVaultResult8+import com.peachpasswords.vault.UnlockResult9+import com.peachpasswords.vault.VaultEntry10+import dagger.hilt.android.EntryPointAccessors11+import kotlinx.coroutines.runBlocking12+import org.junit.Assert.assertEquals13+import org.junit.Assert.assertTrue14+import org.junit.Test15+import org.junit.runner.RunWith16+17+/**18+ * Emulator-QA harness: seeds a real vault in-process so autofill and19+ * keyboard-row chip testing never depends on IME text injection (Gboard20+ * corrupts `adb input text`). Idempotent — safe to re-run; existing vaults21+ * and entries are left untouched.22+ *23+ * Deliberately activity-free: the onboarding screen runs an infinite Compose24+ * animation, so `ActivityScenario.launch`'s waitForIdleSync never returns.25+ * The repository is fetched straight from Hilt instead.26+ *27+ * The master password below is QA-only and lives solely on throwaway28+ * emulator vaults.29+ */30+@RunWith(AndroidJUnit4::class)31+class VaultSeedTest {32+33+    private val masterPassword = "Tulip9Marble42"34+35+    @Test36+    fun seedVaultWithLoginsForAutofillQA() {37+        val context = ApplicationProvider.getApplicationContext<Context>()38+        val vault = EntryPointAccessors39+            .fromApplication(context, VaultRepositoryEntryPoint::class.java)40+            .vaultRepository()41+        runBlocking {42+            if (!vault.hasVault()) {43+                assertEquals(CreateVaultResult.Success, vault.createVault(masterPassword))44+            }45+            if (!vault.isUnlocked.value) {46+                assertEquals(UnlockResult.Success, vault.unlock(masterPassword))47+            }48+            if (vault.getEntries().isEmpty()) {49+                vault.addEntry(login("GitHub", "https://github.com"))50+                vault.addEntry(login("Proton Mail", "https://account.proton.me/login"))51+                vault.addEntry(login("Vercel", "https://vercel.com/login"))52+            }53+            val entries = vault.getEntries()54+            assertEquals(3, entries.size)55+            assertTrue(entries.all { it.password != null && it.url != null })56+        }57+    }58+59+    private fun login(title: String, url: String) = VaultEntry(60+        id = "",61+        type = "login",62+        title = title,63+        username = "june",64+        password = "Correct-Horse-$title-42",65+        url = url,66+        createdAt = 0,67+        modifiedAt = 0,68+    )69+}

view file

app/src/main/java/com/peachpasswords/di/VaultModule.kt+9 −0
@@ -2,10 +2,12 @@22 33 import com.peachpasswords.sync.PairedDeviceRegistry44 import com.peachpasswords.sync.PairedDeviceStore5+import com.peachpasswords.vault.VaultRepository56 import com.peachpasswords.vault.VaultStorage67 import com.peachpasswords.vault.VaultStore78 import dagger.Binds89 import dagger.Module10+import dagger.hilt.EntryPoint911 import dagger.hilt.InstallIn1012 import dagger.hilt.components.SingletonComponent1113 import javax.inject.Singleton@@ -20,4 +22,11 @@2022     @Binds2123     @Singleton2224     abstract fun bindPairedDeviceRegistry(impl: PairedDeviceStore): PairedDeviceRegistry25+}26+27+/** QA seam: lets instrumented tests reach the vault without any UI. */28+@EntryPoint29+@InstallIn(SingletonComponent::class)30+internal interface VaultRepositoryEntryPoint {31+    fun vaultRepository(): VaultRepository2332 }

view file