Permalink
Browse files

Added assertions for screen definitions.

This makes for nicer DSL to describe changes declaratively
rather than programmatically.
  • Loading branch information...
1 parent 63cea3d commit 6fc29068b8fe1ac3e687f0c36074c655b7b1ea19 @alexvollmer alexvollmer committed Sep 30, 2010
Showing with 280 additions and 10 deletions.
  1. +64 −0 README.md
  2. +204 −10 assertions.js
  3. +11 −0 screen.js
  4. +1 −0 tuneup.js
View
64 README.md
@@ -50,12 +50,75 @@ the way). For now though, the basic assertions supported are:
* `assertNotNull`
* `assertEquals`
+ * `assertMatch`
* `assertTrue`
* `assertFalse`
+ * `assertWindow` (more on this below)
* `fail`
See the `assertions.js` file for all the details.
+## Window Assertions ##
+A common theme in writing integration tests for "screen flows" is the
+repetitive cycle of making several assertions on a screen, then engaging some
+user-control after all of the assertions pass. With the UIAutomation API as it
+is, it's easy to lose sight of this structure when bogged down in the syntax of
+asserting and navigating the user interface.
+
+To make this cycle more obvious, and cut down on unnecessary verbosity, use the
+`assertWindow` function. It works by applying a given JavaScript object literal
+to the current main window (UIAWindow instance).
+
+The full details are documented in `assertions.js`, but here's a taste of what
+this assertion can do for your tests. Prior to `assertWindow` you would have
+to do something like this:
+
+ test("my test", function(app, target) {
+ mainWindow = app.mainWindow();
+ navBar = mainWindow.navigationBar();
+ leftButton = navBar.leftButton();
+ rightButton = navBar.rightButton();
+
+ assertEquals("Back", leftButton.name());
+ assertEquals("Done", rightButton.name());
+
+ tableViews = mainWindow.tableViews();
+ assertEquals(1, tableViews.length);
+ table = tableViews[0];
+
+ assertEquals("First Name", table.groups()[0].staticTexts()[0].name());
+ assertEquals("Last Name", table.groups()[1].staticTexts()[0].name());
+
+ assertEquals("Fred", table.cells()[0].name());
+ assertEquals("Flintstone", table.cells()[1].name());
+ });
+
+With `assertWindow`, you can boil it down to this:
+
+ test("my test", function(app, target) {
+ assertWindow({
+ navigationBar: {
+ leftButton: { name: "Back" },
+ rightButton: { name: "Done" }
+ },
+ tableViews: [
+ {
+ groups: [
+ { name: "First Name" },
+ { name: "Last Name" }
+ ],
+ cells: [
+ { name: "Fred" },
+ { name: "Flintstone" }
+ ]
+ }
+ ]
+ });
+ });
+
+You can do more than just match string literals. Check out the full
+documentation in `assertions.js` for all the details.
+
## `UIAutomation` Extensions ##
The `UIAutomation` library is pretty full-featured, but is a little wordy.
@@ -75,3 +138,4 @@ See the `uiautomation-ext.js` for details.
# Copyright #
Copyright (c) 2010 Alex Vollmer. See LICENSE for details.
+
View
214 assertions.js
@@ -1,4 +1,11 @@
/**
+ * Just flat-out fail the test with the given message
+ */
+function fail(message) {
+ throw message;
+}
+
+/**
* Asserts that the given expression is true and throws an exception with
* a default message, or the optional +message+ parameter
*/
@@ -17,8 +24,8 @@ function assertTrue(expression, message) {
* @param message - an optional string message
*/
function assertMatch(regExp, expression, message) {
- if (! message) message = "'" + expression + "' does not match '" + regExp + "'";
- assertTrue(regExp.test(expression), message);
+ var defMessage = "'" + expression + "' does not match '" + regExp + "'";
+ assertTrue(regExp.test(expression), message ? message + ": " + defMessage : defMessage);
}
/**
@@ -27,8 +34,8 @@ function assertMatch(regExp, expression, message) {
* a default message, or the one given as the last (optional) argument
*/
function assertEquals(expected, received, message) {
- if (! message) message = "Expected " + expected + " but received " + received;
- assertTrue(expected == received, message);
+ var defMessage = "Expected <" + expected + "> but received <" + received + ">";
+ assertTrue(expected == received, message ? message + ": " + defMessage : defMessage);
}
/**
@@ -45,13 +52,200 @@ function assertFalse(expression, message) {
* a default message or the given optional +message+ parameter
*/
function assertNotNull(thingie, message) {
- if (message == null) message = "Expected not null object";
- assertTrue(thingie != null && thingie.toString() != "[object UIAElementNil]", message);
+ var defMessage = "Expected not null object";
+ assertTrue(thingie != null && thingie.toString() != "[object UIAElementNil]",
+ message ? message + ": " + defMessage : defMessage);
}
/**
- * Just flat-out fail the test with the given message
+ * Assert that the given window definition matches the current main window. The
+ * window definition is a JavaScript object whose property hierarchy matches
+ * the main UIAWindow. Property names in the given definition that match a
+ * method will cause that method to be invoked and the matching to be performed
+ * and the result. For example, the UIAWindow exposes all UITableViews through
+ * the tableViews() method. You only need to specify a 'tableViews' property to
+ * cause the method to be invoked.
+ *
+ * PROPERTY HIERARCHY Property definitions can be nested as deeply as
+ * necessary. Matching is done by traversing the same path in the main
+ * UIAWindow as your screen definition. For example, to make assertions about
+ * the left and right buttons in a UINavigationBar you can do this:
+ *
+ * assertWindow({
+ * navigationBar: {
+ * leftButton: { name: "Back" },
+ * rightButton: ( name: "Done" },
+ * }
+ * });
+ *
+ * PROPERTY MATCHERS For each property you wish to make an assertion about, you
+ * can specify a string, number regular expression or function. Strings and
+ * numbers are matches using the assertEquals() method. Regular expressions are
+ * matches using the assertMatch() method.
+ *
+ * If you specify 'null' for a property, it means you don't care to match.
+ * Typically this is done inside of arrays where you need to match the number
+ * of elements, but don't necessarily care to make assertions about each one.
+ *
+ * Functions are given the matching property as the single argument. For
+ * example:
+ *
+ * assertWindow({
+ * navigationBar: {
+ * leftButton: function(button) {
+ * // make custom assertions here
+ * }
+ * }
+ * });
+ *
+ * ARRAYS
+ * If a property you want to match is an array (e.g. tableViews()), you can
+ * specify one of the above matchers for each element of the array. If the
+ * number of provided matchers does not match the number of given elements, the
+ * assertion will fail (throw an exception)
+ *
+ * In any case, you specify another object definition for each property to
+ * drill-down into the atomic properties you wish to test. For example:
+ *
+ * assertWindow({
+ * navigationBar: {
+ * leftButton: { name: "Back" },
+ * rightButton: ( name: "Done" },
+ * },
+ * tableViews: [
+ * {
+ * groups: [
+ * { name: "First Group" },
+ * { name: "Second Group" }
+ * ],
+ * cells: [
+ * { name: "Cell 1" },
+ * { name: "Cell 2" },
+ * { name: "Cell 3" },
+ * { name: "Cell 4" }
+ * ]
+ * }
+ * ]
+ * });
+ *
+ * HANDLING FAILURE If any match fails, an appropriate exception will be
+ * thrown. If you are using the test structure provided by tuneup, this will be
+ * caught and detailed correctly in Instruments.
+ *
+ * POST-PROCESSING If your screen definition provides an 'onPass' property that
+ * points to a function, that function will be invoked after all matching has
+ * been peformed on the current window and all assertions have passed. This
+ * means you can assert the structure of your screen and operate on it in one
+ * pass:
+ *
+ * assertWindow({
+ * navigationBar: {
+ * leftButton: { name: "Back" }
+ * },
+ * onPass: function(window) {
+ * var leftButton = window.navigationBar().leftButton();
+ * leftButton.tap();
+ * }
+ * });
*/
-function fail(message) {
- throw message;
-}
+function assertWindow(window) {
+ target = UIATarget.localTarget();
+ application = target.frontMostApp();
+ mainWindow = application.mainWindow();
+
+ try {
+ if (window.onPass) {
+ var onPass = window.onPass;
+ delete window.onPass;
+ }
+ assertPropertiesMatch(window, mainWindow, 0);
+
+ if (onPass) {
+ onPass(mainWindow);
+ }
+ }
+ catch(badProp) {
+ fail("Failed to match " + badProp[0] + ": " + badProp[1]);
+ }
+};
+
+/**
+ * Asserts that the +expected+ object matches the +given+ object by making
+ * assertions appropriate based on the type of each property in the
+ * +expected+ object. This method will recurse through the structure,
+ * applying assertions for each matching property path. See the description
+ * for +assertWindow+ for details on the matchers.
+ */
+function assertPropertiesMatch(expected, given, level) {
+ for (var propName in expected) {
+ if (expected.hasOwnProperty(propName)) {
+ try {
+ var expectedProp = expected[propName];
+ var givenProp = given[propName];
+
+ if (typeof(givenProp) == "function") {
+ try {
+ // We have to use eval (shudder) because calling functions on
+ // UIAutomation objects with () operator crashes
+ // See Radar bug 8496138
+ givenProp = eval("given." + propName + "()");
+ }
+ catch (e) {
+ UIALogger.logDebug("Unable to evaluate given." + propName + "() against " + given);
+ continue;
+ }
+ }
+
+ // null indicates we don't care to match
+ if (expectedProp == null) {
+ continue;
+ }
+
+ if (!givenProp) {
+ throw propName;
+ }
+
+ if (typeof(expectedProp) == "string") {
+ assertEquals(expectedProp, givenProp);
+ }
+ else if (typeof(expectedProp) == "number") {
+ assertEquals(expectedProp, givenProp);
+ }
+ else if (typeof(expectedProp) == "function") {
+ if (expectedProp.constructor == RegExp) {
+ assertMatch(expectedProp, givenProp);
+ }
+ else {
+ expectedProp(givenProp);
+ }
+ }
+ else if (typeof(expectedProp) == "object") {
+ if (expectedProp.constructor == Array) {
+ assertEquals(expectedProp.length, givenProp.length, "Incorrect number of elements in array");
+ for (var i = 0; i < expectedProp.length; i++) {
+ var exp = expectedProp[i];
+ var giv = givenProp[i];
+ assertPropertiesMatch(exp, giv, level + 1);
+ };
+ }
+ else if (typeof(givenProp) == "object") {
+ assertPropertiesMatch(expectedProp, givenProp, level + 1);
+ }
+ else {
+ throw propName;
+ }
+ }
+ }
+ catch(e) {
+ if (typeof(e) == "string") {
+ throw [propName, e];
+ }
+ else {
+ e[0] = propName + "." + e[0];
+ throw e;
+ }
+ }
+ }
+ }
+};
+
View
11 screen.js
@@ -0,0 +1,11 @@
+Screen = {
+ screens: new Object(),
+
+ add: function(name, definition) {
+ this.screens[name] = definition;
+ },
+
+ named: function(name) {
+ return this.screens[name];
+ }
+};
View
1 tuneup.js
@@ -1,4 +1,5 @@
#import "assertions.js"
#import "uiautomation-ext.js"
#import "lang-ext.js"
+#import "screen.js"
#import "test.js"

0 comments on commit 6fc2906

Please sign in to comment.