90bb6598d4cb
test: emulator QA harness for autofill chips — vault seed + native form probe
- time
- 2026-08-05 00:40 UTC · 2 months ago
- machine
- koh2
- parent
- 4a5b20b003e3
↓ peach-android-90bb6598.tar.gzatlas at this save
files changed5 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 −0app/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>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+}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+}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+}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 }