appium / appium-flutter-driver

Appium Flutter Driver is a test automation tool for Flutter apps on multiple platforms/OSes. Appium Flutter Driver is part of the Appium mobile test automation tool maintained by community
MIT License
461 stars 183 forks source link

How to get one of the elements if the finder matched more than one? #140

Closed hex0cter closed 2 years ago

hex0cter commented 3 years ago

With the finder from appium-flutter-driver, sometimes I got more than one element returned. Then if I do another API call, it returns Bad state: Too many elements. For example, in the code below, if there are more than one widgets with the same text:

from appium_flutter_finder import FlutterElement, FlutterFinder

finder = FlutterFinder()
text_finder = finder.by_text(text)
element = FlutterElement(driver, text_finder)

print(element.text)

the last line will raise:

(<class 'selenium.common.exceptions.WebDriverException'>, WebDriverException('An unknown server-side error occurred while processing the command. Original error: Cannot execute command get_text, server reponse {\n  "isError": true,\n  "response": "Uncaught extension error while executing get_text: Bad state: Too many elements\\n#0      Iterable.single (dart:core/iterable.dart:560)\\n#1      FlutterDriverExtension._getText (package:flutter_driver/src/extension/extension.dart:625)\\n<asynchronous suspension>\\n#2      FlutterDriverExtension.call (package:flutter_driver/src/extension/extension.dart:273)\\n<asynchronous suspension>\\n#3      BindingBase.registerServiceExtension.<anonymous closure> (package:flutter/src/foundation/binding.dart:547)\\n<asynchronous suspension>\\n",\n  "type": "_extensionType",\n  "method": "ext.flutter.driver"\n}', None, None), <traceback object at 0x102059180>)

If I call element[0].text, I will get (<class 'TypeError'>, TypeError("'FlutterElement' object is not subscriptable"), <traceback object at 0x10d001600>).

So my question is, how can I get one of the elements if the finder matched several widgets? Any help is appreciated. 🙏

shibupanda commented 3 years ago

@hex0cter Below link may help I guess flutter community has not yet exposed finders to find elements and perform action in the basis of index. Work around to assign different key.

https://github.com/flutter/flutter/issues/22868

hex0cter commented 3 years ago

Thanks for confirming this @shibupanda. One more question, is there any nice way to check the number of matched elements without getting the exception?

In the example above, before calling element.text, we don't know if the element exists at all, or if more than one elements are matched. In either case, when I call element.text, I only get exceptions.

GowthamReddyAmbati commented 2 years ago

@hex0cter I'm also looking for a solution to the same issue.

litan1984 commented 2 years ago

Facing same issue, while identifying dynamic list view widget with same type.

KazuCocoa commented 2 years ago

Perhaps currently this is a flutter (or flutter_driver by Flutter team) limitation.

ehabibov commented 1 year ago

+1, it is annoying not having option to get list of elements. I also received "Bad state: Too many elements" recently and was looking for possible solutions.

There are Finders that could return multiple items but it is very hard (imo) to interact with if exposed. _fluttertest has such one abstract class - Finder.

Finder class

Searches a widget tree and returns nodes that match a particular pattern.

allCandidatesIterable<Element> Returns all the Elements that will be considered by this finder.

Well, as I already spent time on that, here are some obvious things:

Some traces how client <->driver_extension commands work:

Additionally command handler on extension side: https://github.com/flutter/flutter/blob/master/packages/flutter_driver/lib/src/extension/extension.dart#L367

It is not clear how search is done under hood as we can only log client<->driver_extension interaction, but not driver_extension<->app. And here some examples of outgoing/incoming payloads:

[debug] [FlutterDriver] >>> {"command":"waitFor","finderType":"ByText","text":"Skip for now","timeout":10000}
[debug] [FlutterDriver] <<< {"isError":false,"response":{},"type":"_extensionType","method":"ext.flutter.driver"} | previous command waitFor

[debug] [FlutterDriver] >>> {"command":"get_text","finderType":"ByText","text":"Skip for now"}
[debug] [FlutterDriver] <<< {"isError":false,"response":{"text":"Skip for now"},"type":"_extensionType","method":"ext.flutter.driver"} | previous command get_text

[debug] [FlutterDriver] >>> {"command":"tap","finderType":"ByText","text":"Skip for now"}
[debug] [FlutterDriver] <<< {"isError":false,"response":{},"type":"_extensionType","method":"ext.flutter.driver"} | previous command tap
martin-braun commented 4 months ago

@ehabibov Tell me something, without telling me something. ;)

Anyways, you can approach this problem by using descendant with firstMatchOnly:

  SerializableFinder findFirst({required SerializableFinder of}) =>
      find.descendant(
          of: find.byType("MaterialApp"), matching: of, firstMatchOnly: true);