This answer demonstrates how to embed a link within an annotated string and make it clickable. This works great and triggers the on click with the correct URL. However, I can't seem to write a test that clicks the annotated text to open the link. Has anyone had success writing a test like this? My production code is very similar to what is in the answer. Below is my test code:
#Test
fun it_should_open_terms_of_service_link() {
val termsOfServiceText = getString(R.string.settings_terms)
try {
Intents.init()
stubAnyIntent()
composeTestRule.onNode(hasText(termsOfServiceText, substring = true)).performClick()
assertLinkWasOpened(getString(R.string.settings_terms_link))
} finally {
Intents.release()
}
}
It looks like hasText(termsOfServiceText, substring = true) fetches the entire annotated string node opposed to just the substring, "Terms of Service". Thus, the on click method does get triggered, just not at the correct position in the annotated string. Happy to provide more info if needed. Thanks!
You can perform the click at an offset. This performs the click at the tail end horizontally, and in the middle vertically.
composeTestRule.onNode(hasText(termsOfServiceText, substring = true))
.performGesture { click(percentOffset(.9f, .5f)) }
The disadvantage is that it makes the test more fragile.
Related
I took a Codelab Lunch-tray App it had no Tests so I tried to create these tests to practice. I tried to create testcases for it based on another codelab Codelab Cupcake
The way these 2 projects differ is that on the second codelab(Lunch-tray) the "Next" button is in uppercase.
Which I can not figure out how to write a test to make it pass.
val myNextButton = composeTestRule.activity.getString(R.string.btn_next).uppercase()
composeTestRule.onNodeWithText(myNextButton).performClick()
navController.assertCurrentRouteName(LixoPlayScreen.SideDishScreen.name)
This seems to work, but on mouse over activity I see this message
Avoid calling often as it can involve synchronization and can be slow.
In the onNodeWithText method you can set the ignoreCase to true.
Something like:
composeTestRule.onNodeWithText(
text = composeTestRule.activity.getString(R.string.next),
ignoreCase = true)
.performClick()
In the codelab, to use the onNodeWithStringId method just change:
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.onNodeWithStringId(
#StringRes id: Int
): SemanticsNodeInteraction = onNodeWithText(activity.getString(id),ignoreCase = true)
This might be a very silly question, but I am logging the methods that are triggered in my app as strings. When an issue is submitted, I would like to automatically input the text of the strings as parameters for methods. E.g:
For method:
fun assignPot(potType: PotType, ball: DomainBall, action: PotAction) {...}
I'd like to somehow call method:
assignPot(FOUL(2, BLUE(5), SWITCH))
From String:
"FOUL(2, BLUE(5), SWITCH)"
The only workaround I can think of is to split the string and create a when -> then function to get actual classes from strings, but I wondered if there's a more concise way for this.
This is not what you want to do. You should design your app in a way that prevents users from providing input similar to actual code.
However, you can achieve this. Complex parsings like this oftenly use regex-based approaches.
As you said, you should map your string part to class. If your PotType is enum, you can do something like
val re = Regex("[^A-Za-z\s]")
val reNumbers = Regex("[^0-9\s]")
// get classes
val classNames = originalString.replace(re, "").split(" ")
// get their constructor numerical arguments
val classArgs = originalString.replace(reNumbers, "").split(" ")
After that you can implement mapping with when expression. You probably will use some collection of Any type.
As you see, this sadly leads you to parsing code by code. Concise way to solve is to implement your own script compiler/interpreter and use it in your application :) That will later lead you to dealing with security breaches and so on.
If you are logging problematic method calls and want to repeat them immediately after issue is submitted, you probably want to programatically save the calls to lambdas and call them when you receive an issue log.
i am new to Android development with Kotlin pogramming language, i am not able to understand this code below,
what i am guessing is that an instance(scanResultAdapter) is created from ScanResultAdapter class, this class has the code for recyclerView adapter.
This code is in MainActivity.kt file in punchThrough's article about BLE -
private val scanResultAdapter: ScanResultAdapter by lazy {
ScanResultAdapter(scanResults) { result ->
// User tapped on a scan result
if (isScanning) {
stopBleScan()
}
with(result.device) {
Log.w("ScanResultAdapter", "Connecting to $address")
connectGatt(context, false, gattCallback)
}
}
}
Which point you don't understand?
Regards your guess(the logic of code), that's right, I think.
It seems that you are not familiar with Kotlin, you can check them one by one. For an instance, I don't know this style code:
ScanResultAdapter(scanResults) { result ->
....
}
{ result -> .. }, I need to know that's the Lambdas Expressions
Here's a reference
Maybe I don't understand the with of Kotlin, refer to this document and so on.
You are right. (scanResults) is the first argument of constructor. Second is callback of click listener in curly brackets. When user tap on a device in the list this code in braces {} will be executed. Kotlin can take out of parentheses () arguments if it is functions.
I am working with overload functions in Kotlin.
In this schematic example, suppose a function whose only difference is the type of view that I pass to the function. One uses TextView, the other uses Button, so I have 2 different functions.
fun workWithViews(v:TextView,...){
// code
}
fun workWithViews(v:Button,...){
// same code!
}
In this case, the properties I use are the same (isAllCaps, gravity, etc.). The problem is that I have to place the same code twice, i.e., the whole code is exactly the same.
It happens because isAllCaps (just like many other properties) it is not a general property of a view, but of some types of views
So it doesn't work, because obviously the compiler see the function parameter, not the real parameter.
I also can make function with a view type parameter, with a big when with my type possibilities:
fun workWithTextView(v:View,...){
when{
(v is TextView) -> {
// code
}
(v is Button) {
// same code
}
} // when
}
The 2 solutions are terrible and generate duplicate code or boilerplate.
I can also do the when before each access to some field, which makes things even worse. Now imagine if one has 5 similar types instead of 2, with many fields in common.
I read some suggestions to create union types in Kotlin. It would be great!
For instance:
fun workWithViews(v:(TextView, Button),...){
// just one code repetition....
}
or
union textBut = TextView , Button
fun workWithViews(v:textBut ,...){
// just one code repetition....
}
In that case I would only have to test a certain type (if (v is typeX)) if I used something specific for that type.
Is there some best solution?
Button is a subclass of TextView, so you can make the function signature take a TextView and put Button-specific stuff in an if-block.
fun workWithTextView(textView: TextView) {
// Do stuff common to TextViews and Buttons.
if (textView is Button) {
// Do extra stuff only for Buttons.
}
}
If Views have common methods, then most likely one view extends the other.
For example, Button extends TextView and you can do this:
fun workWithViews(v:TextView){
// TextView code
}
fun workWithViews(v:Button){
// Button specific code
workWithViews(v as TextView)
}
I have an Instrument test that has four match statements in it. There is one of the four statements that fails when ran with the other three, but passes if it runs by itself.
here is the test that fails
#Test
fun displayTypeFilteredPokemon(){
// When - PokemonList fragment launch and filtered by specific type
launchActivity()
onView(withId(R.id.genAllButton)).perform(click())
// Perform click action to do the filter on specific type
onView(withId(R.id.menu_filter)).perform(click())
onView(withText(R.string.label_type_electric)).perform(click())
// Then - Verify the list is filtered by the selected type
onView(withId(R.id.pokemon_list)).check(RecyclerViewItemCountAssertion(3))
onView(withText("Magnemite")).check(matches(isDisplayed()))
onView(withText("Jolteon")).check(matches(isDisplayed()))
onView(withText("Emolga")).check(matches(isDisplayed()))
}
here is the code for launch activity:
private fun launchActivity(): ActivityScenario<PokemonListActivity>? {
val scenario = launch(PokemonListActivity::class.java)
scenario.onActivity {
val intent = Intent()
intent.putExtra("genId",0)
it.intent = intent
}
return scenario
}
And here is the custom matcher code:
class RecyclerViewItemCountAssertion(private val matcher: Int) : ViewAssertion {
override fun check(view: View?, noViewFoundException: NoMatchingViewException?) {
if(noViewFoundException != null){
throw noViewFoundException
}
val recyclerView = view as RecyclerView
val adapter = recyclerView.adapter!!
assertThat(adapter.itemCount, IsEqual(matcher))
}
}
when using this set of matches it passes:
onView(withId(R.id.pokemon_list)).check(RecyclerViewItemCountAssertion(3))
onView(withText("Jolteon")).check(matches(isDisplayed()))
onView(withText("Emolga")).check(matches(isDisplayed()))
or when this set of matches it passes as well:
onView(withText("Magnemite")).check(matches(isDisplayed()))
here is the view being tested:
I am slightly confused because the view clearly has the matching text there. Could it be the resource isn't idling, therefor the test just closes? For example the reason the test with only one matcher passes is due to being fast enough to match before it finishes?
I have thought about introducing EspressoIdlingResource but I have read that introduces difficulties in a code base and I would like to keep it simple for learning purposes.
Another indicator that I think the race condition is an issue is that when I debug the test it passes. When I just run the test it fails.
Edit 1:
when running all the test by itself, I have no errors. When running all the tests in the class I get the following error:
androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: with id: com.stegner.androiddex:id/menu_filter
Your approach to writing espresso tests seems wrong.
While writing a UI test, what we want to confirm is the following.
"If user interacts clicks button X, swipes the page, writes 500 to this edittext, and clicks button Y, he will be able to see text Z".
As you can see, we are testing the app as if we are the end user and by just interacting with the elements we see in the screen.
private fun launchActivity(): ActivityScenario<PokemonListActivity>? {
val scenario = launch(PokemonListActivity::class.java)
scenario.onActivity {
val intent = Intent()
intent.putExtra("genId",0)
it.intent = intent
}
return scenario
}
When you use launchActivity and pass a data with an intent, you disregard what we want to test,end users can't pass data with intents, they just click to a button which triggers our code to pass the data with intents. So a good test, should be from end to end (from launch screen to the screen you want) with mimicking user's movements(click X, click Y, write 500 to Z etc)
Now your problem is most likely resulted from using launchActivity in every single one of your tests inside a method, the second test can't launch the Activity (at least it can't launch the activity fast enough) so you get an error in your first assertion.
androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: with id: com.stegner.androiddex:id/menu_filter
You should remove launchActivity() from your test methods and define an activity test rule using the first screen your app displays.
#Rule
public ActivityTestRule<MyFirstActivity> activityRule =
new ActivityTestRule<>(MyFirstActivity.class);
When you annotate this class with the annotation Rule, before every test starts it will make sure this activity is loaded and then you can go to the page you want with mimicking user interactions. If you still have problems after applying this solution, please write it as a comment because then your problem will be related to synchronization which requires a different answer.