Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions onebusaway-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -428,13 +428,13 @@ dependencies {
implementation(libs.gtfs.realtime.bindings)
implementation(libs.proto.google.common.protos)

// Jetpack Compose on the 1.11.x line (BOM 2026.05.00 -> compose-ui/foundation 1.11.2, material3
// Jetpack Compose on the 1.11.x line (BOM 2026.06.01 -> compose-ui/foundation 1.11.4, material3
// 1.4.0). This required the coordinated toolchain bump to compileSdk 37 + AGP 9.2.0 + Gradle 9.4.1.
// The BOM manages compose-ui + material3 (no explicit versions).
val composeBom = platform(libs.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
implementation(libs.compose.ui) // 1.11.2 via BOM
implementation(libs.compose.ui) // 1.11.4 via BOM
implementation(libs.compose.ui.tooling.preview) // @Preview support
debugImplementation(libs.compose.ui.tooling) // preview renderer (debug only)
// Compose UI instrumented testing (createComposeRule): semantic tree assertions on real devices,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package org.onebusaway.android.ui.arrivals
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
Expand All @@ -28,6 +27,7 @@ import org.onebusaway.android.R
import org.onebusaway.android.ui.arrivals.components.RouteArrivalRow
import org.onebusaway.android.ui.arrivals.components.previewArrival
import org.onebusaway.android.ui.arrivals.components.previewRowCallbacks
import org.onebusaway.android.ui.compose.createUnconfinedComposeRule

/**
* On-device regression test for the per-row service-alert indicator (issue #1687 Bug 2), which sits
Expand All @@ -39,10 +39,9 @@ import org.onebusaway.android.ui.arrivals.components.previewRowCallbacks
*/
class ArrivalAlertIndicatorTest {

// See EtaStripJustifyTest for why the v1 (Unconfined) rule is used here (issue #1792).
@Suppress("DEPRECATION")
// See createUnconfinedComposeRule for why Unconfined composition is used here (issue #1792).
@get:Rule
val composeRule = createComposeRule()
val composeRule = createUnconfinedComposeRule()

@Test
fun showsTappableAlertIndicatorWhenArrivalHasActiveAlert() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.junit.Rule
import org.junit.Test
import org.onebusaway.android.ui.arrivals.components.EtaStrip
import org.onebusaway.android.ui.arrivals.components.previewArrival
import org.onebusaway.android.ui.arrivals.components.previewRowCallbacks
import org.onebusaway.android.ui.compose.createUnconfinedComposeRule

/**
* On-device regression test for the strip justifying to its first non-negative ETA (recent-past
Expand All @@ -38,14 +38,10 @@ import org.onebusaway.android.ui.arrivals.components.previewRowCallbacks
*/
class EtaStripJustifyTest {

// The suggested androidx.compose.ui.test.junit4.v2.createComposeRule runs composition on a
// StandardTestDispatcher instead of Unconfined; under it, this composable's Modifier.onGloballyPositioned
// callback never fires at all (confirmed via on-device logcat — composition reaches the pill, but the
// callback never runs within a 5s poll), not just the expected LaunchedEffect timing shift. Tracked in
// https://github.com/OneBusAway/onebusaway-android/issues/1792.
@Suppress("DEPRECATION")
// Unconfined composition — see createUnconfinedComposeRule for why (this composable's
// measure -> onGloballyPositioned -> LaunchedEffect -> scrollTo chain, and issue #1792).
@get:Rule
val composeRule = createComposeRule()
val composeRule = createUnconfinedComposeRule()

/** A strip that overflows its host: the recent-past pills must scroll off to justify. */
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
Expand All @@ -33,13 +32,13 @@ import org.onebusaway.android.R
import org.onebusaway.android.ui.arrivals.components.ArrivalRowCallbacks
import org.onebusaway.android.ui.arrivals.components.RouteArrivalRow
import org.onebusaway.android.ui.arrivals.components.previewArrival
import org.onebusaway.android.ui.compose.createUnconfinedComposeRule

class RouteArrivalRowLongPressTest {

// See EtaStripJustifyTest for why the v1 (Unconfined) rule is used here (issue #1792).
@Suppress("DEPRECATION")
// See createUnconfinedComposeRule for why Unconfined composition is used here (issue #1792).
@get:Rule
val composeRule = createComposeRule()
val composeRule = createUnconfinedComposeRule()

@Test
fun longPressOpensScheduleWithoutOverflowButton() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2026 Open Transit Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onebusaway.android.ui.compose

import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.v2.createComposeRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher

/**
* The Compose test rule this project's on-device UI tests should use.
*
* It calls the non-deprecated v2 `createComposeRule`, but pins composition to an
* `UnconfinedTestDispatcher` through the rule's `effectContext` parameter (which, per its contract,
* uses any `TestDispatcher` found there for composition and the `MainTestClock`). This runs
* composition coroutines eagerly, matching the semantics these tests were written and validated
* against, and matching the old — now deprecated — v1 `createComposeRule`. The point of this helper
* is to drop that deprecation suppression suite-wide (issue #1792) without changing test behavior.
*
* Note on the v2 default (`StandardTestDispatcher`, which *queues* composition coroutines rather than
* running them eagerly): when #1792 was filed, the `onGloballyPositioned` -> `LaunchedEffect` ->
* `animateScrollTo` chains in `EtaStrip`/`SlideBox` never advanced to completion under it. That
* upstream gap is gone on compose-ui-test 1.11.4 (the rule's `waitUntil`/idle machinery now pumps the
* dispatcher) and all six tests pass under the plain default too — but we pin Unconfined because
* `SlideBoxTest` is an `@SmokeTest` on the API 23 floor emulator, whose StandardTestDispatcher timing
* is unvalidated.
*
* See https://github.com/OneBusAway/onebusaway-android/issues/1792 for the investigation.
*/
@OptIn(ExperimentalCoroutinesApi::class)
fun createUnconfinedComposeRule(): ComposeContentTestRule =
createComposeRule(UnconfinedTestDispatcher())
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.dp
import org.junit.Rule
import org.junit.Test
import org.onebusaway.android.SmokeTest
import org.onebusaway.android.ui.compose.createUnconfinedComposeRule

/**
* On-device tests for the SlideBox's single-owner anchor-chasing glide. This is the regression test
Expand All @@ -37,11 +37,10 @@ import org.onebusaway.android.SmokeTest
@SmokeTest // API-23 floor smoke subset (#1818): exercises Compose rendering on a 2015-era runtime
class SlideBoxTest {

// See EtaStripJustifyTest for why the deprecated rule (Unconfined composition) is kept — under
// the v2 rule this project's onGloballyPositioned/effect chains never run (#1792).
@Suppress("DEPRECATION")
// See createUnconfinedComposeRule for why Unconfined composition is used here — this is the
// API 23 floor @SmokeTest, so eager execution matters most (issue #1792).
@get:Rule
val composeRule = createComposeRule()
val composeRule = createUnconfinedComposeRule()

/** The anchor offset the content declares, in px — mutated mid-test to drive the glide. */
private val anchor = mutableIntStateOf(ANCHOR_PX)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
package org.onebusaway.android.ui.home.arrivals

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
Expand All @@ -21,14 +20,14 @@ import org.onebusaway.android.ui.arrivals.AlertItem
import org.onebusaway.android.ui.arrivals.AlertSeverity
import org.onebusaway.android.ui.arrivals.ArrivalsUiState
import org.onebusaway.android.ui.arrivals.StopHeader
import org.onebusaway.android.ui.compose.createUnconfinedComposeRule
import org.onebusaway.android.time.ServerTime

class ServiceAlertsDialogTest {

// See EtaStripJustifyTest for why the v1 (Unconfined) rule is used here (issue #1792).
@Suppress("DEPRECATION")
// See createUnconfinedComposeRule for why Unconfined composition is used here (issue #1792).
@get:Rule
val composeRule = createComposeRule()
val composeRule = createUnconfinedComposeRule()

@Test
fun summaryOpensTheSelectedAlert() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.test.platform.app.InstrumentationRegistry
Expand All @@ -29,13 +28,13 @@ import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.onebusaway.android.R
import org.onebusaway.android.ui.compose.createUnconfinedComposeRule

class FocusBannerTest {

// See EtaStripJustifyTest for why the v1 (Unconfined) rule is used here (issue #1792).
@Suppress("DEPRECATION")
// See createUnconfinedComposeRule for why Unconfined composition is used here (issue #1792).
@get:Rule
val composeRule = createComposeRule()
val composeRule = createUnconfinedComposeRule()

private val context = InstrumentationRegistry.getInstrumentation().targetContext

Expand Down