Inspired by a hybrid of WindowLicker (for Java) and Drew Bourne's Mockolate (for AS3).
RobotEyes should really be called RobotEyes and RobotFingers. It's called RobotEyes because it was developed for RobotLegs... but then I removed all the RobotLegs dependencies. Ho hum.
The aim is to deliver end-to-end testing to AS3 applications.
Currently I'm only adding functionality as I actually need to make use of it, so feel free to fork and expand.
Functionality included in this version has been tested by incorporation in an end-to-end test of an application. It may well do unexpected things.
In TDD speak (Test Driven Development), an end-to-end test is one that proves that your application carries out one of your user stories successfully.
A good example would be that, given a certain viable user/password combination, when the login button is clicked the login procedure completes and whatever application state change is required occurs. Or - that given a blank username, clicking doesn't submit the details to the login service. Or - that given a crappy user/password combination, the login fails and the application handles this gracefully, with user feedback... you get the idea.
End-to-end testing is different from Unit Testing and is additional to it - not a replacement for it. It aims to provide certainty that your actual application works, not just your individual classes, and, in doing so, it'll usually include coverage of any backend services as well.
RobotEyes is designed to integrate with your Unit Testing framework - it was developed for ASUnit 3 but doesn't have any dependencies.
The idea is that you set your tests up to use RobotEyes as the user: both to visually verify that certain things appear how they should, and to enter text, press buttons, operate scrollers and so on. RobotEyes provides for this by supplying you with a driver wrapping your UIElements / Views.
If your test requires a TextField nested within a certain view you can grab a driver for that view using:
var textDriver:TextFieldDriver = inViewOf(LoginPanel).getA(TextField).named("username_txt");
The TextFieldDriver provides some useful functions:
textDriver.enterText('User A');
... this sets the text, but also fires the TextEvent.TEXT_INPUT event before, and the Event.CHANGE event after, just as if a real people type person had edited it.
Then you can also use the TextFieldDriver.checkText('Blah') function in a test:
assertTrue('TextField starts blank', (textDriver.(checkText(''))));
If you want to be specific about the view containing the view you're hooking into - for example, if you have a view that recurs in your app you can nest 'inViewOf' - start at the outside and work inward:
var textDriver:TextFieldDriver = inViewOf(StatsReportScreen).inViewOf(DateTimeWidget).getA(TextField).named("weekdayAbbreviation_txt");
var buttonDriver:InteractiveObjectDriver = inViewOf(InterestingView).getA(Sprite).withProperty('name', 'btn_login');
buttonDriver.click();
This will dispatch a MouseEvent.CLICK from the button / sprite / whatever you specified.
List of helpers for the InteractiveObjectDriver: click(); mouseOver(); mouseOut(); mouseDown(); mouseUp(); rollOver(); rollOut();
var loginScreenDriver:DisplayObjectDriver = inViewOf(ShellContextView).getA(LoginPanel);
var loginScreen:DisplayObject = loginScreenDriver.view;
assertTrue('LoginPanel is off screen', (loginScreen.y < (-loginScreen.height)));
Count instances of a specific type - for example to check that the correct number of instances have been added to a list
var listHolderDriver:DisplayObjectDriver = inViewOf(SomeApp).getA(ListHolder);
assertEquals('Correct number of items added to the list', 8, listHolderDriver.countInstancesOf(ListItem));
To use RobotEyes you need to grab an instance of it and tell it which class to use to start up your application. You probably don't want to start your tests instantly - give the app a little room to breathe if it has async startup stuff.
In AsUnit 3 this is done with:
// these are the really important RobotEyes classes
import com.newloop.roboteyes.core.RobotEyes;
import com.newloop.roboteyes.inViewOf;
// grab any drivers that you need to use - currently there are only 3 but more to come...
import com.newloop.roboteyes.drivers.DisplayObjectDriver;
import com.newloop.roboteyes.drivers.InteractiveObjectDriver;
import com.newloop.roboteyes.drivers.TextFieldDriver;
// the main class for the app you want to test
import com.client.app.AcademyShell;
public class RobotEyesTest extends TestCase {
private var robotEyes:RobotEyes;
public function RobotEyesTest(methodName:String=null) {
super(methodName)
}
override public function run():void{
if(robotEyes==null){
// AcademyShell is the main class for this application (ie the one the compiler gets fed)
robotEyes = new RobotEyes(AcademyShell);
addChild(robotEyes);
}
// need to wait a while
var timer:Timer = new Timer(1000,1);
timer.addEventListener(TimerEvent.TIMER, timerHandler);
timer.start();
}
// we run the tests after a second has passed just to give the application a chance to step through any set up
private function timerHandler(e:TimerEvent):void{
super.run();
}
....
In an end-to-end test you'll probably need to test some stuff asynchronously, and in ASUnit you'd do this by:
public function testEndToEndProcessPass():void{
var textFieldDriver:TextFieldDriver = inViewOf(ShellContextView).inViewOf(LoginPanel).getA(TextField).named("username_txt") as TextFieldDriver;
textFieldDriver.enterText('Pass');
var buttonDriver:InteractiveObjectDriver = inViewOf(ShellContextView).inViewOf(LoginPanel).getA(Sprite).withProperty("name", "btn_login") as InteractiveObjectDriver;
// we're going to wait 4 seconds for the dummy service to return, and the login screen to tween away before we test this
var asyncTest:Function = addAsync(null, 4000,loginShouldBeOffScreen);
// simulate clicking the button
buttonDriver.click();
}
public function loginShouldBeOffScreen(e:Event):void{
var loginScreenDriver:DisplayObjectDriver = inViewOf(ShellContextView).getA(LoginPanel);
var loginScreen:DisplayObject = loginScreenDriver.view;
assertTrue('LoginPanel is off screen', (loginScreen.y < (-loginScreen.height)));
}
RobotEyes generally currently returns null whenever it can't find something. This will almost certainly then cause your test to throw a null pointer error, so first it will throw an Error with message: 'RobotEyes can't find an instance of [Class:WidgetView] inside [Object:SpiffingSectionView]'.
Fork, message me on git or email [email protected]. I'm @stray_and_ruby on twitter.