Frank icon indicating copy to clipboard operation
Frank copied to clipboard

Extend protocol to allow map across specific subset of elements

Open lukeredpath opened this issue 11 years ago • 6 comments

I'm currently experimenting with a Ruby automation testing API that mirrors Apple's Javascript UIAutomation API, building on top of Frank and the Igor selector engine to do the heavy lifting.

One core difference is that UIAutomation specifically distinguishes between a collection of elements (which I can easily represent with an Igor query) and a single element - in UIAutomation, interactions and queries only happen against a single element.

My current approach is for my UIAElement objects to keep a track of their index and this works fine for queries - I can have x number of UIATableView objects, for example, represented by the same Igor query, and just pick out the specific result returned by Frank's map command.

Where this approach falls down is for behaviour, e.g. touches or other user interactions. In this case, I want to explicitly only perform a given selector against a single object within a set returned by a given query.

My suggestion is that the /map API is extended to allow clients to either specify an index, or a range. I'm happy to implement this, but I wanted to get some feedback on the best approach to this.

I'm currently leaning towards being able to specify a start and end index as an array:

{
  'query': 'tableViewCell marked:\'Touch Me!\'',
  'selector_engine': 'shelley_compat',
  'range': [0, 3]
  'operation': {
    'method_name': 'touch',
    'arguments': []
  }
}

That would apply the 'touch' selector to the first four elements returned by the query.

Alternatively, you could define the API such that you specify the start index and a length, instead. That would make the above command apply to the first three elements returned by the query.

You could also support specifying specific indexes:

{
  'query': 'tableViewCell marked:\'Touch Me!\'',
  'selector_engine': 'shelley_compat',
  'indexes': [0, 2, 3]
  'operation': {
    'method_name': 'touch',
    'arguments': []
  }
}

Finally, the protocol should define what would happen if you were to specify an invalid range (start or end index out of bounds) or invalid indexes - you could return a nil result for each invalid index although this could potentially be confusing. Alternatively some kind of error could be returned.

Any thoughts?

lukeredpath avatar Apr 16 '13 18:04 lukeredpath

Hi Luke,

Any thoughts?

Range and index are ways to select views. It seems to me that the best place to implement those concepts is in the selector engine(s), not in the map method.

Another part of the problem is what to do when the selector finds more or fewer views than you want. I can see a common need to match exactly one view. Perhaps a map_unique method, or an optional "unique" flag in the parameters.

Do you commonly run into situations where you want to perform an operation only if the selector engine finds, say, exactly three views?

Dale

Dale Emery Consultant to software teams and leaders http://dhemery.com

dhemery avatar Apr 16 '13 18:04 dhemery

On 16 Apr 2013, at 19:36, Dale Emery [email protected] wrote:

Hi Luke,

Any thoughts?

Range and index are ways to select views. It seems to me that the best place to implement those concepts is in the selector engine(s), not in the map method.

I don't agree. I've already selected my views, I just want the map operation to apply to a specific subset of those selected views. Pushing this into the selector engine would mean each engine would need to support this in its own specific way.

Another part of the problem is what to do when the selector finds more or fewer views than you want. I can see a common need to match exactly one view. Perhaps a map_unique method, or an optional "unique" flag in the parameters.

Do you commonly run into situations where you want to perform an operation only if the selector engine finds, say, exactly three views?

As I said, I'm trying to implement an API that mirrors UIAutomation which requires a different approach to selecting views. I know I have 10 buttons, but I want to just work with the second one, for instance.

Specifying a range is possibly me over thinking the solution. What I really want to do is perform a selector against a specific view by specifying an index within a collection, rather than constructing a query that will only return one view.

lukeredpath avatar Apr 16 '13 18:04 lukeredpath

You sometimes do need to select a view at a specific index within a selector statement. A (contrived) example is that you want to tap the first button in the second table row in a table. To do that you'd write a selector which selects the 2nd row and then the first button. You can't easily do that without the ability to select by index within a selector.

I'm also a bit wary of the complexity that this would add to the protocol. Lots of different error cases.

I like Dale's idea of being able to say "map the first element that matches, or raise an error if nothing matches". Or possibly "map the one element that matches, or raise and error if there isn't exactly one match". Maybe both? The latter would be a useful default to use when using map to simulate an interaction - you almost always want to simulate it on one and only one element.

Luke, what do you think? Would that approach work for your current needs?

Cheers,

Pete

Typed on a little bitty keyboard

On Apr 16, 2013, at 11:44 AM, Luke Redpath [email protected] wrote:

On 16 Apr 2013, at 19:36, Dale Emery [email protected] wrote:

Hi Luke,

Any thoughts?

Range and index are ways to select views. It seems to me that the best place to implement those concepts is in the selector engine(s), not in the map method.

I don't agree. I've already selected my views, I just want the map operation to apply to a specific subset of those selected views. Pushing this into the selector engine would mean each engine would need to support this in its own specific way.

Another part of the problem is what to do when the selector finds more or fewer views than you want. I can see a common need to match exactly one view. Perhaps a map_unique method, or an optional "unique" flag in the parameters.

Do you commonly run into situations where you want to perform an operation only if the selector engine finds, say, exactly three views?

As I said, I'm trying to implement an API that mirrors UIAutomation which requires a different approach to selecting views. I know I have 10 buttons, but I want to just work with the second one, for instance.

Specifying a range is possibly me over thinking the solution. What I really want to do is perform a selector against a specific view by specifying an index within a collection, rather than constructing a query that will only return one view. — Reply to this email directly or view it on GitHub.

moredip avatar Apr 17 '13 16:04 moredip

Oh and the big caveat that I forgot to mention us that selecting by index should always be a last resort anyway. It leads to very fragile selectors and the order of elements is often unintuitive.

Cheers,

Pete

Typed on a little bitty keyboard

On Apr 16, 2013, at 11:44 AM, Luke Redpath [email protected] wrote:

On 16 Apr 2013, at 19:36, Dale Emery [email protected] wrote:

Hi Luke,

Any thoughts?

Range and index are ways to select views. It seems to me that the best place to implement those concepts is in the selector engine(s), not in the map method.

I don't agree. I've already selected my views, I just want the map operation to apply to a specific subset of those selected views. Pushing this into the selector engine would mean each engine would need to support this in its own specific way.

Another part of the problem is what to do when the selector finds more or fewer views than you want. I can see a common need to match exactly one view. Perhaps a map_unique method, or an optional "unique" flag in the parameters.

Do you commonly run into situations where you want to perform an operation only if the selector engine finds, say, exactly three views?

As I said, I'm trying to implement an API that mirrors UIAutomation which requires a different approach to selecting views. I know I have 10 buttons, but I want to just work with the second one, for instance.

Specifying a range is possibly me over thinking the solution. What I really want to do is perform a selector against a specific view by specifying an index within a collection, rather than constructing a query that will only return one view. — Reply to this email directly or view it on GitHub.

moredip avatar Apr 17 '13 16:04 moredip

This is a working implementation I have working locally. It takes a string that can be passed into NSRangeFromString. If the range is not valid or not valid across the found views, it simply returns an error. Its naive but simple:

From cbb845493de13cacbec15914858208a8c424985c Mon Sep 17 00:00:00 2001
From: Luke Redpath <[email protected]>
Date: Wed, 17 Apr 2013 15:06:50 +0100
Subject: [PATCH] Allow clients to limit a map operation to a specific
 sub-range of the results returned by the UI query selector.

Lets lets clients form a query to, for instance, find all table view
cells in a table view, and then tap just one of those without having
to construct a more specific query, simply by specifying a 1-length
range, starting with the cells index.

An invalid range or range that falls outside the bounds of the objects
returned by selector will cause an error to be returned.
---
 src/MapOperationCommand.m | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/MapOperationCommand.m b/src/MapOperationCommand.m
index 24f2a77..2e311b2 100644
--- a/src/MapOperationCommand.m
+++ b/src/MapOperationCommand.m
@@ -54,8 +54,21 @@
        NSLog( @"Exception while using %@ to select views with '%@':\n%@", selectorEngineString, selector, e );
        return [FranklyProtocolHelper generateErrorResponseWithReason:@"invalid selector" andDetails:[e reason]];
    }
+  
+  NSString *mappingRangeInput = [requestCommand objectForKey:@"range"];
+  if (mappingRangeInput) {
+    NSRange mappingRange = NSRangeFromString(mappingRangeInput);
+    @try {
+      viewsToMap = [viewsToMap subarrayWithRange:mappingRange];
+    }
+    @catch (NSException *exception) {
+      NSLog(@"Exception while trying to restrict views to map to range: %@, %@", mappingRangeInput, exception);
+      return [FranklyProtocolHelper generateErrorResponseWithReason:@"Invalid range" andDetails:[exception reason]];
+    }
+  }

     NSMutableArray *results = [NSMutableArray arrayWithCapacity:[viewsToMap count]];
+  
    for (FrankMapViewType *view in viewsToMap) {
        @try {
            id result = [self performOperation:operation onView:view];
-- 
1.7.11.4+GitX

lukeredpath avatar Apr 17 '13 16:04 lukeredpath

I don't completely agree with

that selecting by index should always be a last resort anyway

The index shouldn't be overused but it still has its uses, especially when testing widgets displaying dynamic data (e.g. table views, collection views etc). A few days ago I was writing a test checking out whether cells in a table are sorted correctly. Such a simple test case is almost impossible to write with Shelley because the cell with index 0 doesn't have to be the first one. Since cell views are dynamically added/removed, their index after a sequence of operations is totally random.

Before implementing changes like index ranges, the ordering should be solved.

ondrejhanslik avatar Apr 19 '13 21:04 ondrejhanslik