What is the best way to test the different DayNight themes on Android using Espresso? (or something better?) I haven't found anything on the internet. I thought this must be something big because of everything migrating to DayNight now.
I want to know things like: "when I click this button, has my activity theme changed" or "I have this background and this text, is the contrast right".
Thanks in advance.
I have found this to work:
#get:Rule
val activityTestRule = ActivityScenarioRule(...Activity::class.java)
private fun createActivityScenarioRule(withNightMode: Boolean = false) =
activityTestRule.scenario.apply {
onActivity {
AppCompatDelegate.setDefaultNightMode(
if (withNightMode) AppCompatDelegate.MODE_NIGHT_YES
else AppCompatDelegate.MODE_NIGHT_NO
)
}
}
Related
I'm trying to test a Text that on my component I can print it in different colors, so on my test I'm verifying it gets the expected color. I was looking for a method to return the color but I did not find any.
From now I'm asserting that the text is correct and the visibility is correct, but when trying to find the method to get the colour I get too deep and I'm looking for a simpler solution.
composeTestRule.onNode(hasTestTag("testTagForButton"), true)
.assertExists()
.assertTextEquals("Testing")
I've check that I can do something like .fetchSemanticsNode().layoutInfo.getModifierInfo() to get into the Modifier and perhaps from there I can get the colour, but it's too much maybe. Also I've found this .captureToImage() that perhaps I could get the colour on it, but since I had to put the pixels I decided that it's not the way.
Is there any simple way to get that?
I am by no means a compose expert, but just looking at compose source code, you could utilize their GetTextLayoutResult accessibility semantic action. This will contain all the properties that are used to render the Text on a canvas.
Some quick and dirty extension functions I put up for convenience:
fun SemanticsNodeInteraction.assertTextColor(
color: Color
): SemanticsNodeInteraction = assert(isOfColor(color))
private fun isOfColor(color: Color): SemanticsMatcher = SemanticsMatcher(
"${SemanticsProperties.Text.name} is of color '$color'"
) {
val textLayoutResults = mutableListOf<TextLayoutResult>()
it.config.getOrNull(SemanticsActions.GetTextLayoutResult)
?.action
?.invoke(textLayoutResults)
return#SemanticsMatcher if (textLayoutResults.isEmpty()) {
false
} else {
textLayoutResults.first().layoutInput.style.color == color
}
}
Which can be then used like this:
composeTestRule.onNode(hasTestTag("testTagForButton"), true)
.assertExists()
.assertTextEquals("Testing")
.assertTextColor(Color.Black)
I am unable to comment post above, also didn't find question about checking background color, so decide to place my version here.
private fun hasBackground(node: SemanticsNode, color: Color, shape: Shape): Boolean {
return node.layoutInfo.getModifierInfo().filter { modifierInfo ->
modifierInfo.modifier == Modifier.background(color, shape)
}.size == 1
}
To test background color and don't touch debug inspection info (this isn't for testing) we are unable to test only background color, but can test whole background by comparing production background (which placed into modifier) with our testing one.
Hope it help somebody.
The new API SplashScreen in Android 12 seems good but just like before the sample code in the documentation does not really help explaining the whole and proper implementation. There is also some cases where you might do some task during splash screen in our case this is to launch Firebase Auth so probably the best way is just to opt out on using this new featured API but according to lint warning it seems like it is mandatory and there is no way to opt out.
The application should not provide its own launch screen
Application-defined Launch Screen Starting in Android 12 (API 31+),
the application's Launch Screen is provided by the system and the
application should not create its own, otherwise the user will see two
splashscreen. Please check the SplashScreen class to check how the
Splash Screen can be controlled and customized.
How about the backward compatibility for older devices, how to handle it? Is there any codelab project to play and test with?
Can we opt out of SplashScreen?
It looks like we can't opt out as Android Team is trying to unify the app loading experience: https://9to5google.com/2021/04/21/android-12-dp3-all-apps-now-show-the-same-splash-screen-while-loading-gallery/
How to use it?
If you don't do anything then it will use windowBackground of the theme & your launcher icon & dismissed as soon as your app draws its first frame.
There are bunch of properties that you can modify like background, icon etc: https://developer.android.com/about/versions/12/features/splash-screen#set-theme
What if I want splash to stay longer? Like fetching a local DataBase.
You can use ViewTreeObserver.OnPreDrawListener & make a blocking call from your viewmodel return if it's ready to go ahead.
Activity:
// My Launcher Activity
class MainActivity : AppCompatActivity() {
private val viewModel : JustDelayViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
// Check if the initial data is ready.
return if (viewModel.getIsReady()) {
// The content is ready; start drawing.
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
// The content is not ready; suspend.
false
}
}
}
)
}
}
ViewModel:
class JustDelayViewModel : ViewModel() {
fun getIsReady(): Boolean {
val result = viewModelScope.runCatching {
runBlocking {
//do some blocking call check for Firebase result or something
delay(5000)
}
true //return the result
}
return result.isSuccess
}
}
You can read more about this: https://developer.android.com/about/versions/12/features/splash-screen#suspend-drawing
To complement Mayur's answer for older device support.
The new windowSplashScreen* attributes need to be added in the res/values-v31/style.xml file.
Then for the legacy splashscreen it depend of the current implementation of the app.
If the application simply uses a starting theme with a custom windowBackground there is nothing to do since the windowBackground isn't used for the new splash screen (only if it's a simple color).
If the application has some visible splash screen Activity, there will be a double splash screen on Android 12. To solve this, the application can migrate to the windowBackground solution.
If the application really need to keep its splash screen Activity, it can update the layout to match the system splash screen on Android 12 and/or create a smooth transition from the system splash screen to the app splash screen using the SplashScreen.setOnExitAnimationListener()
We can also use android's splash screen library - link
android {
compileSdk 31
...
}
dependencies {
...
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
}
This will give splash screen options in style.xml, you just need to create 2 style.xmls 1 for android api 31 and above and one of below api 31
<style name="Theme.CustomSplashScreenTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">#color/white</item>
<item name="windowSplashScreenAnimatedIcon">#drawable/logo</item>
<item name="windowSplashScreenAnimationDuration">300</item>
<item name="postSplashScreenTheme">#style/Theme.YourAppTheme</item>
</style>
Learn more about this library using this example
you can add this line:
<item name="android:windowIsTranslucent">true</item>
in your style.xml file before close style tag. it`s make your default android splash transparent!
For tests I use Espresso and Barista
I have a test in which I need to open another screen by pressing a button. How can I check if this screen opens? Did the screen I need open?
Can I somehow check the chain of screens? To understand that the screens open in the order I need?
If someone throws links to good tutorials on UI tests in Android, I will be very grateful.
An easy solution would be to just check for an element of the new screen to be shown like this:
onView(withId(R.id.id_of_element_in_your_new_screen)).check(matches(isDisplayed()))
If you really want to check out for the current activity that is shown, you could try something like this:
Gather the current activity via InstrumentationRegistry and check for the activity in stage RESUMED.
fun getTopActivity(): Activity? {
InstrumentationRegistry.getInstrumentation().runOnMainSync {
val resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
if (resumedActivities.iterator().hasNext()) {
resumedActivities.iterator().next()?.let {
activity = it
}
}
}
return activity
}
You could then check this in a test like this:
#Test
fun checkForActivity() {
val currentActivity = getTopActivity()
assertTrue(currentActivity?.javaClass == YourActivityToCheckAgainst::class.java)
}
I personally use intended(hasComponent(YourActivityToCheckAgainst::class.java.name)), which checks if the last intent was done with a desired activity, set as its component.
I also wrote an extensive Android UI testing tutorial using Espresso + Barista libraries.
I'm trying to support the Android Q Dark theme for my Android app and I can't figure out how to import different assets based on the theme I'm currently in.
Im using the official DayNight theme for making the dark/light versions and for drawables is very easy to just point to the XML and it will choose the correct value either from values or values-night depending on what is enabled.
I wanted to do something similar where depending on the theme it would load either the asset "priceTag_light.png" or "priceTag_dark.png".
val inputStream = if(darkIsEnabled) {
assets.open("priceTag_dark.png")
} else {
assets.open("priceTag_light.png")
}
Is there a way I get that flag?
Okay finally found the solution I was looking for. As #deepak-s-gavkar points out the parameter that gives us that information is on the Configuration. So, after a small search I found this article that gives this example method that has worked perfectly for what I wanted:
fun isDarkTheme(activity: Activity): Boolean {
return activity.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
You first need to do this changes in manifest
<activity
android:name=".MyActivity"
android:configChanges="uiMode" />
then onConfigurationChanged of activity
val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> {} // Night mode is not active, we're using the light theme
Configuration.UI_MODE_NIGHT_YES -> {} // Night mode is active, we're using dark theme
}
Use the following code:
boolean isDarkThemeOn = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
I'm having quite a bit of trouble getting the DayNight theme to play nicely after a preference change and activity.recreate() call. Depending on the value (and which SDK I'm on), I get inconsistent theming (icons are dark on dark or light on light).
In my PreferenceFragment, I have a preference that allows the user to set one of three values: Light, Dark, or Auto, which correspond with AppCompatDelegates MODE_NIGHT_NO, MODE_NIGHT_YES, or MODE_NIGHT_AUTO, respectively. Here's what the implementation looks like:
PreferenceFragment.kt
override fun onPreferenceChange(preference: Preference, value: Any): Boolean {
setSummary(preference, value.toString())
return when (preference.key) {
themePreference.key -> consume {
AppCompatDelegate.setDefaultNightMode(appSharedPreferences.string(R.string.preference_theme_key).toInt())
activity?.recreate()
}
}
}
BaseActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
AppCompatDelegate.setDefaultNightMode(appSharedPreferences.string(R.string.preference_theme_key).toInt())
super.onCreate(savedInstanceState)
}
I've also tried combinations of:
delegate.setLocalNightMode(appSharedPreferences.string(R.string.preference_theme_key).toInt())
and
AppCompatDelegate.setDefaultNightMode(appSharedPreferences.string(R.string.preference_theme_key).toInt()
peppered into different places in the activity/application lifecycle with no success.
What I'm seeing mostly is dark icons on a dark status bar, and inconsistencies on pre-P app switcher (the theme is Night, but the app switcher toolbar shows up as a light theme). I don't appear to have issues with the theme of my own app controls, like tabs or text - it's mainly on the android system views like the status bar and app switcher toolbar).
If I kill the app and re-launch, I have no issues whatsoever. It's only after an activity.recreate call do I see these issues.
After searching for a simple solution for a long while, I found an attribute that conveniently handles this scenario. I'm not sure how proper this approach is, but from my testing it works as expected.
Given that you're using Theme.MaterialComponents.DayNight or some similar variant in your style XML, you can use ?attr/isLightTheme as a true/false flag.
<item name="android:windowLightStatusBar">?attr/isLightTheme</item>
The value returned by ?attr/isLightTheme is handled by the system. You can check the current value with:
AppCompatDelegate.getDefaultNightMode();
// or
Configuration configuration = getResources().getConfiguration();
int currentNightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;