This is a follow up to this post on how to use Frank with RSpec.
I have found that unfortunately Frank has some really sad documentation. The easiest thing to do is just read their source code. This file is the most useful. But even then there’s some issues I’ve run into. Once you get the hang of the things covered in this post, you should be pretty set. The rest is just using RSpec’s many matchers to verify things happened the way you expected.
Selectors
Frank’s website has good documentation for selectors. Go get familiar with them. Selectors are how you do EVERYTHING in Frank, so if you aren’t good at them, you will struggle. Also one thing to note, is how powerful the selector language is. You can modify it to do anything you need.
Basically everything that goes into a selector is going to be somehow evaluated for each view. Suppose you have the following selector
1 |
view:'UIButton' marked:'Touch Me' |
In a very basic sense, the way this works, is Shelly will start looking through the view hierarchy and start asking views ‘are you a UIButton’ and once it has all the buttons that said yes, it will ask all of them ‘are you marked Touch Me’. The interesting thing about how shelly works is that these methods are implemented by shelley as categories on UIView.
The way the selectors get parsed makes it so the language is infinitely extendable. For example, let’s say we wanted to do something like this
1 |
view textColor:'Blue' |
Where we want to just find a view that has blue text. The only thing we have to do is create a category in our application’s source on UIView (or whatever class we care about) and implement -(BOOL)textColor:(NSString*)color;
When we do this, the shelly engine will be parsing the selector and happen across this ‘textColor’ thingy and just start asking objects in the view hierarchy ‘do you respond to the “textColor:” selector?’ and if a view says yes, it will just execute the selector. If the return value is YES the view is included in the results set, if the return value is NO, or if the view does not respond to the selector, then the view is excluded from the results set.
It is an infinitely extendable selector language. Any time you start running into weird issues like how to select something from a table view that isn’t currently on screen, the first place I go is to the application’s source code. (I imagine some people out there actually have to go ask a developer to do this for them, but I’m lucky to be the developer, and the automation engineer)
frankly_map
Basically everything uses frankly_map at some point or another. It’s not my favorite way of doing things, but it’s simple, and understanding it can go a long way. Here’s it’s method signature:
1 |
- (Array) frankly_map(selector, method_name, *method_args) |
And it’s purpose from the documentation:
Ask Frank to execute an arbitrary Objective-C method on each view which matches the specified selector.
The first parameter is the selector for all of the affected objects. The second parameter is the objective-c selector such as “tableView:cellForRowAtIndexPath:” and after that you’ll have all of your arguments for the method you’re calling.
So let’s say you have a text field and you want to get the text from it
1 |
frankly_map "view:'UITextField'", 'text' |
This will return an array of all of the text values for every UITextField on screen. In order for it to be more valuable, you’d probably want a more specific selector.
See, one little oddity to Frank is that when you query the UI, you are going to get back only what you asked for, and nothing else. Say you want to get back an object representing a UITextField – you’re out of luck. You can’t just get back an array of objects representing a UI component, and look through the array until you find the one you care about. Instead you have to get back just the specific properties you need. It sounds odd at first, but you get good at it pretty quick
Touching
One of the problems I ran into with touch is that pretty much everything is touch, wait for nothing to be animating, touch wait for nothing to be animating, and so on. It was frustrating to run a test just to find that I accidentally did an RSpec matcher while something was animating. So here’s a little helper method I came up with
1 2 3 4 5 |
def touch_async(selector) wait_for_nothing_to_be_animating 5 touch(selector) wait_for_nothing_to_be_animating 5 end |
Typing
If you look at the user contributed steps, you’ll see a lot of things like this
1 2 3 |
touch( text_field_selector ) frankly_map( text_field_selector, 'setText:', text_to_type ) frankly_map( text_field_selector, 'endEditing:', true ) |
or like this
1 2 3 |
frankly_map( text_field_selector, 'becomeFirstResponder' ) frankly_map( text_field_selector, 'setText:', text_to_type ) frankly_map( text_field_selector, 'endEditing:', true ) |
or whatever the hell this thing is
1 2 3 4 5 6 7 8 9 10 |
def send_command ( command ) %x{osascript<<APPLESCRIPT tell application "System Events" tell application "iPhone Simulator" to activate keystroke "#{command}" delay 1 key code 36 end tell APPLESCRIPT} end |
The problem with the last one is that it’s finicky, and if you do anything while your tests are running you’re screwed. The problem with the first two is that they aren’t actually simulating typing. They’re setting the text. Say you have some validation on a field as you’re typing, or that you have some auto suggestions or searching or something, it won’t work.
In Cocoa, when you set text, or change selection or something programmatically, it doesn’t go through all of the delegate methods. It is assumed that if you change something programmatically, you know about it (duh).
There’s an actual method in Frank to type text in with the keyboard. Problem is, it tries to type too fast so it doesn’t work. Here’s what i’ve come up with for the best way to type text. It’s a tiny bit slower than if you just set the text, but it’s also going to properly simulate what a user would do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
def type_text(selector, text, options = {}) options = { :append_return => false, :resign_after_typing => false, :clear_before_typing=>true }.merge(options) touch_async(selector) if options[:clear_before_typing] current_text = frankly_map(selector, 'text').first delete = (0...current_text.length).map{"\b".chr}.join type_into_keyboard(delete, :append_return => false) end text.each_char do |letter| type_into_keyboard letter, :append_return => false sleep 0.1 end if options[:resign_after_typing] frankly_map(selector, 'resignFirstResponder') elsif options[:append_return] type_into_keyboard '' end end |
wait_for_nothing_to_be_animating
There’s a fun story with this one. First off, if you have a text field that is the first responder (has keyboard focus) then wait_for_nothing_to_be_animating will just not work. The reason is there’s some issue with the way the keyboard works in iOS 7 that makes it so the keyboard always says it’s animating. There’s also apparently some issues with other controls, but I don’t know what they are off hand.
The best solution I came up with to fix this problem is to just modify the Shelly source code. By the time some of you read this, they may have finally fixed this, but right now they haven’t and I don’t have time to wait for them to approve a pull request. Clone (or download a zip) the Shelly repo, and then in the iOS file UIView+ShelleyExtensions.m replace the isAnimating method with this
1 2 3 4 5 6 7 8 9 10 |
- (BOOL) isAnimating { if([self isKindOfClass:NSClassFromString(@"UIKeyboardSliceTransitionView")]) return NO; if ([self respondsToSelector:@selector(motionEffects)]) { return (self.layer.animationKeys.count > [[self performSelector: @selector(motionEffects)] count]); } else { return (self.layer.animationKeys.count > 0); } } |
This will do what they were already doing, but ignoring the UIKeyboardSliceTransitionView which was the one causing the problem.