Find multiple elements in web view with espresso - android

I'm testing a hybrid app, where each view has a web view.
In one of these web views I have a list of elements with the same attribute. They have the same xpath locator that is something like:
//h4[contains(#data-role, 'product-name')]
I want to create a list of these elements and iterate through them, count them, get their attributes.
In the documentation, I found two similar methods:
findElement(locator, value)
and
findMultipleElements(locator, value)
Though it's totally unclear to me how to use it. I tried to find examples on it but with no success.
Could someone help me with this?

Here is the solution that I have found.
#kaqqao is right that findMultipleItems call returns Atom<List<ElementReference>> that is not usable with onWebView() because there you have only withElement() that accepts either Atom<ElementReference> or just ElementReference
What you can do though is perform your action that find multiple items and just get results from your Atom. This is how it works internally if you check the source of doEval method inside Web.java for espresso.
val elements = with(AtomAction(findMultipleElements(
Locator.XPATH,
"YOUR_COMPLEX_XPATH"
), null, null)) {
onView(ViewMatchers.isAssignableFrom(WebView::class.java)).perform(this)
this.get()
}
This code will give you List<ElementMatcher>.
Then just run it as
elements.forEach {
onWebView().forceJavascriptEnabled().withElement(it).perform(webClick())
}

Can you try something like that? Since what you should care about is really the ElementReference and you can iterate the lsit returned from findMultipleElements with simple for/foreach statement:
yourList = findMultipleElements(locator, value);
yourList.size(); //this will get you the count of found elements with that locator
for(Atom<ElementReference> item : yourList ){
item.getAttribute...
//and whatever you want
}

Related

list adapter with sections

I already have a list adapter that works properly. But I want to divide the object in the list into sections according to the date they were created. Something like this:
I found something called "sectioned recycler view" but couldn't find any documentation on that. I read all the related questions, but they all are either outdated or use a third-party library. What's the native way of implementing this feature?
There are a couple of approaches you could use. First the easy one:
make the header part of your item layout, but with GONE visibility by default
in onBindViewHolder, decide whether the header should be VISIBLE or GONE
The logic there depends on what you want, but it could be as simple as
val visible = position == 0 || items[position].date != items[position - 1].date
Basically you just need to work out what the condition is that would cause an item to be in a different "group" than the previous item, and then if it's met, show the header over that item.
The approach MarkL is talking about is more complex, but it's also more robust - by having separate Item and Header elements, you can treat them differently, and even do stuff like having the header collapse/show its children, select them all etc. You can do that with the other approach, but it requires more code since you're not really treating things as groups, it's more of a trick when it comes to displaying stuff.
Basically, ignoring the how for now, you need a list of headers and items. A sealed class is a good way to represent that:
sealed class ListElement {
data class Header(val date: Date) : ListElement()
data class Item(val itemData: YourItem) : ListElement()
}
I've made Item a wrapper class that holds your actual data object inside, since that's probably coming from elsewhere and you can't define it as part of this sealed class hierarchy - so sticking it inside one of the subclasses like this allows you to do that.
So now you can have a List<ListElement> containing Headers and Items in display order. Since you've mentioned creating the ViewHolders in a comment I won't explain all that, but basically when you're getting the item type for a position, you just need to check is Header or is Item and then handle it from there.
As for creating that list, there are lots of ways to do it - you could use groupBy to generate a Map of dates to lists of items, map each of those entries to a list of Header, Item, Item..., and flatten the whole thing into a single list:
items.map { Item(it) }
.groupBy { it.itemData.date }
.entries
.flatMap { (date, items) -> listOf(Header(date)) + items }
The advantage with a map like this is it's an actual grouped structure, so you can keep it around to generate flat lists for display - e.g. hiding a group's contents by only including the header in the list.
Or you could just build the list yourself, similar to the logic I mentioned in the first example - if the date has changed from the previous item, insert a Header first:
val list = mutableListOf<ListElement>().apply {
for (item in items) {
// add a header if the date changed - this handles the first header
// in an empty list too (where lastOrNull returns null, so the date is null)
val previousItemDate = (lastOrNull() as? Item)?.itemData?.date
if (previousItemDate != item.date) add(Header(item.date))
add(Item(item))
}
}
Or you could use fold. Don't forget to sort stuff!
You could create 2 types of view holders:
header which holds the date
data container which holds the other information.
And then create a list of objects which contain something like this:
listToBind = (header, data, data, header, data, data)
For your case, where header & info is the same object, you can do something like this:
take your object you receive from backend (example)
YourObject(val header: String, val info:InfoObject)
create 2 display objects, both inheriting from a type that your adapter accepts (say - AdapterDisplayEntity)
HeaderDisplayEntity(val header: String): AdapterDisplayEntity
InfoDisplayEntity(val info: InfoObject): AdapterDisplayEntity
now you can use your list that contains these items and submit to your adapter.
Use nested recycler view for this instead. You can check the example here.
Best solution for this scenario so far.
If you are using Jetpack Compose you can use the stickyHeader() as documented in the documentation

Android Java: Filter a list with elements in another list using Stream API

I cannot paste much code because I'm not sure about how to achieve my goal, and every code I write with Stream ends underlined in red.
I know how to filter a list based on a property like:
List<RecordDTO> filteredRecords = Stream.of(records).filter(item -> item.getIdTest().equals(test.getIdTest())).collect(Collectors.toList());
but don't know how to do it based on another list.
My app works with tests and records, and I have a List<Test> testsList where every test has an id, and a List<Record> recordsList where every record has an idTest.
From the full list of records, I need to get the ones containing an idTest belonging to the testsList, but not sure how to do it with Stream.
I'm just trying to avoid the use of for loops and use smarter code, that's why I'm trying to do the filtering with Stream.
Any example?
Well, I think I found a solution by myself. Hope it is useful to anyone else.
List<String> testsIds = Stream.of(tests4Subject).map(TestDTO::getIdTest).collect(Collectors.toList());
List<RecordDTO> filteredRecords = New ArrayList();
if(testsIds!=null)
{
filteredRecords = Stream.of(records).filter(p -> testsIds.contains(p.getIdTest())).collect(Collectors.toList());
}

is it reliable, if i used to check in a list using any before going with foreach in the below example?

Just wondering to check is there any object has isSelected value as true, if so then do a foreach to add the particular object in another list. Is this good way to prevent the foreach or still directly do foreach without any. Because inside the any i can see they do the same foreach. Experts please advice.
final isAnyMainGod = listMainGod.any((element) => element.isSelected);
if (isAnyMainGod) {
listMainGod.forEach((element) {
if (element.isSelected) {
_tempID.add(
FilterData(type: 'MAINGOD', filterValue: element.mainGodNameId));
}
});
}
The issue I see here is that you are doing a potential full iteration of the list, followed up by another full iteration of the list, applying identical logic a second time.
You could do something like this:
var _tempId = listMainGod.where((element) => element.isSelected)
.map((e) => FilterData(type: 'MAINGOD', filterValue: e.mainGodNameId))
.toList();
In the my suggestion, the condition in the where will remove any elements from the list, that don't evaluate to a true. There is still a full iteration of the list (but you'll have to do that anyway at some point), but this suggestion only has 1 full iteration, and a single line of code for the condition (as opposed to your two occurrences of element.isSelected).
Then, the map, is an iteration on only the matching list items, to apply give to the FilterData function.
In the event that no matching elements are found, you are provided a list with 0 elements.
The other added benefit of writing something like this, is it requires less cognitive load, to follow. You don't to know or care if isAnyMainGod exists (or if you have to look and see if it's used elsewhere, because that one spot), you just iterate over any results you have from the result. In my experience, the fewer variables you use to track state results fewer potential places you can introduce bugs.

Couchbase lite on Android, retrieve views

This question is about Couchbase lite (no Sync Gateway).
I'm new to Couchbase, I managed to use the demo app, but I don't understand it completely.
It contains this code which (as far as I understand, since I'm not native English speaker) retrieve views to populate a listview with the indexes:
// This code can be found in ListsActivity.java
// in the setupViewAndQuery() method
com.couchbase.lite.View listsView = mDatabase.getView("list/listsByName");
if (listsView.getMap() == null) {
listsView.setMap(new Mapper() {
#Override
public void map(Map<String, Object> document, Emitter emitter) {
String type = (String) document.get("type");
if ("task-list".equals(type)) {
emitter.emit(document.get("name"), null);
}
}
}, "1.0");
}
listsLiveQuery = listsView.createQuery().toLiveQuery();
Could anyone give me a hand with what each part is doing?
In which step is the listview populated
Can I change "list/listsByName" in the code (line 3)? What would happen?
Can I emit more than one element?
The code is a little bit convoluted. Let's answer the easy parts first.
Can I change "list/listsByName" in the code (line 3)?
Yes. That's just the name of the Couchbase View. You choose the View name. Unfortunately the terms used in Couchbase and Android overlap some. A Couchbase View is a kind of static index of your database.
Can I emit more than one element?
Yes. You can emit most anything you want. Take a look at the documentation here
Now, tracing how the Android ListView gets updated:
In ListsActivity.java notice in the onCreate method a ListAdapter instance gets added to the ListView. This ListAdapter is a private inner class that extends LiveQueryAdapter.
LiveQueryAdapter is in the utils subpackage. If you look at its constructor, you'll see it adds a change listener to the query passed in. When triggered, this change listener sets an enumerator equal to the rows passed back by the live query, then calls notifyDataSetChanged to tell the list to refresh itself. That, in turn, causes getView in ListAdapter to get called. That's where the data is pulled from the database and used to populate a list entry.

Espresso, clicking on a item at position

I'm trying to click on a item at a specific position in a grid view.
onData(instanceOf(MyClass.class))
.inAdapterView(withId(R.id.my_view))
.atPosition(R.integer.my_id)
.perform(click());
but I'm getting this java.lang.IndexOutOfBoundsException: Invalid index 0, size is 0
I'm queuing the responses using MockWebServer, even after the UI is on screen with all the list item, I'm getting this error, I'm not sure why.
Also, I want to get the content of the specific item.
Well, I think that's because you're matching class which is only one, not a specific adapter with values.
Please consider this post:
The matcher passed as argument to onData() must match the value as
returned by Adapter.getItem(). So the first version doesn't match,
because of the wrong type being used. It should be:
onData(is(instanceOf(IconRowAdapter.IconRow.class)))
What also can be a pitfall is using equalTo on different kinds of
CharSequences. String is a CharSequence, but if IconRow.getText()
returns CharSequence instead of String, then this can also be
Spannable, Editable, etc in which case equalTo wouldn't match. So if
IconRow.getText() return anything but String, make sure to convert it
into a String before comparison.
This post was taken from How to use Espresso to test item in adapter at a specific position
Your question lacks of code of tested class, so I cannot give you direct answer. I can only recommend to read StackOverflow link above.
Hope it help
You may need to "drill down" a little deeper into the view hierarchy to get to the item in the cell. Put an additional method call before the ".perform" using the id of the item in the grid cell
onChildView(withId(R.id.???)).perform(click());
Use the id of the view that the user would click on.

Categories

Resources