Grant notification permission before the activity launches

Every instrumented test died with "No compose hierarchies found in the
app". MainActivity.onCreate asks for POST_NOTIFICATIONS before it calls
setContent, and the permission dialog is a separate activity that takes
focus — so the Compose rule attached to a window with no hierarchy yet.

GrantPermissionRule at order = 0, outside the Compose rule at order = 1, so
the grant happens first and no dialog ever appears.

With this, the three fixes in the previous commit, and a relay on the host,
aPersonCanCreateAnAccountOnASelfHostedRelay passes: a real account created
on a real relay, driven by test tags. That is the first end-to-end pass this
harness has produced.

  ./gradlew :app:connectedDebugAndroidTest \
      -Pandroid.testInstrumentationRunnerArguments.relay=10.0.2.2:18810
This commit is contained in:
Paul Hitt
2026-09-02 21:05:03 -04:00
parent 16e0101daf
commit df00bea388
7 changed files with 117 additions and 6 deletions
+3
View File
@@ -225,6 +225,9 @@ dependencies {
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test:runner:1.6.2")
// GrantPermissionRule: permissions have to be granted before the
// activity launches, or its permission dialog steals focus from Compose.
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
@@ -10,6 +10,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.printToString
import androidx.test.rule.GrantPermissionRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assume.assumeTrue
@@ -36,7 +37,24 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ChatFlowTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
/**
* 🔴 Granted BEFORE the activity launches, and that ordering is the point.
*
* `MainActivity.onCreate` asks for POST_NOTIFICATIONS before it calls
* `setContent`. The permission dialog is a separate activity and takes
* focus, so the Compose rule attaches to a window that has no hierarchy
* yet and every test dies with "No compose hierarchies found in the app".
*
* `order = 0` puts this outside the Compose rule, so the grant happens
* first and no dialog ever appears.
*/
@get:Rule(order = 0)
val permissions: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
)
@get:Rule(order = 1)
val compose = createAndroidComposeRule<MainActivity>()
/**
* Which relay to drive.
@@ -10,6 +10,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.printToString
import androidx.test.rule.GrantPermissionRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
@@ -33,7 +34,24 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CrossPlatformTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
/**
* 🔴 Granted BEFORE the activity launches, and that ordering is the point.
*
* `MainActivity.onCreate` asks for POST_NOTIFICATIONS before it calls
* `setContent`. The permission dialog is a separate activity and takes
* focus, so the Compose rule attaches to a window that has no hierarchy
* yet and every test dies with "No compose hierarchies found in the app".
*
* `order = 0` puts this outside the Compose rule, so the grant happens
* first and no dialog ever appears.
*/
@get:Rule(order = 0)
val permissions: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
)
@get:Rule(order = 1)
val compose = createAndroidComposeRule<MainActivity>()
private val args get() = InstrumentationRegistry.getArguments()
private val relay: String get() = args.getString("relay") ?: "relay1.flamenet.io"
@@ -10,6 +10,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.printToString
import androidx.test.rule.GrantPermissionRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assume.assumeTrue
@@ -35,7 +36,24 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CrudSurfaceTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
/**
* 🔴 Granted BEFORE the activity launches, and that ordering is the point.
*
* `MainActivity.onCreate` asks for POST_NOTIFICATIONS before it calls
* `setContent`. The permission dialog is a separate activity and takes
* focus, so the Compose rule attaches to a window that has no hierarchy
* yet and every test dies with "No compose hierarchies found in the app".
*
* `order = 0` puts this outside the Compose rule, so the grant happens
* first and no dialog ever appears.
*/
@get:Rule(order = 0)
val permissions: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
)
@get:Rule(order = 1)
val compose = createAndroidComposeRule<MainActivity>()
/**
* Which relay to drive.
@@ -8,6 +8,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.printToString
import androidx.test.rule.GrantPermissionRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assume.assumeTrue
@@ -40,7 +41,24 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class FeatureUseTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
/**
* 🔴 Granted BEFORE the activity launches, and that ordering is the point.
*
* `MainActivity.onCreate` asks for POST_NOTIFICATIONS before it calls
* `setContent`. The permission dialog is a separate activity and takes
* focus, so the Compose rule attaches to a window that has no hierarchy
* yet and every test dies with "No compose hierarchies found in the app".
*
* `order = 0` puts this outside the Compose rule, so the grant happens
* first and no dialog ever appears.
*/
@get:Rule(order = 0)
val permissions: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
)
@get:Rule(order = 1)
val compose = createAndroidComposeRule<MainActivity>()
/**
* Which relay to drive.
@@ -9,6 +9,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.printToString
import androidx.test.rule.GrantPermissionRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assume.assumeTrue
@@ -50,7 +51,24 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LiveSignUpFleetTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
/**
* 🔴 Granted BEFORE the activity launches, and that ordering is the point.
*
* `MainActivity.onCreate` asks for POST_NOTIFICATIONS before it calls
* `setContent`. The permission dialog is a separate activity and takes
* focus, so the Compose rule attaches to a window that has no hierarchy
* yet and every test dies with "No compose hierarchies found in the app".
*
* `order = 0` puts this outside the Compose rule, so the grant happens
* first and no dialog ever appears.
*/
@get:Rule(order = 0)
val permissions: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
)
@get:Rule(order = 1)
val compose = createAndroidComposeRule<MainActivity>()
private val args get() = InstrumentationRegistry.getArguments()
@@ -9,6 +9,7 @@ import androidx.compose.ui.test.printToString
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.test.rule.GrantPermissionRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assume.assumeTrue
@@ -36,7 +37,24 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SignUpFlowTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
/**
* 🔴 Granted BEFORE the activity launches, and that ordering is the point.
*
* `MainActivity.onCreate` asks for POST_NOTIFICATIONS before it calls
* `setContent`. The permission dialog is a separate activity and takes
* focus, so the Compose rule attaches to a window that has no hierarchy
* yet and every test dies with "No compose hierarchies found in the app".
*
* `order = 0` puts this outside the Compose rule, so the grant happens
* first and no dialog ever appears.
*/
@get:Rule(order = 0)
val permissions: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.POST_NOTIFICATIONS,
)
@get:Rule(order = 1)
val compose = createAndroidComposeRule<MainActivity>()
/**
* Which relay to drive.