diff --git a/build.py b/build.py new file mode 100755 index 0000000..a54edf3 --- /dev/null +++ b/build.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +import optparse +import os +import re +import shutil + +# Location of compiler +MXMLC_PATH = 'mxmlc -debug -verbose-stacktraces -incremental=true -compiler.strict -compiler.show-actionscript-warnings' + +# For replacing .as with .swf +as_re = re.compile('\.as$|\.mxml$') + +def windmill(): + cmd = MXMLC_PATH + ' -source-path=. ./org/windmill/Windmill.as -o ./org/windmill/Windmill.swf' + os.system(cmd) + +def bootstrap(): + cmd = MXMLC_PATH + ' -source-path=. ./org/windmill/WMBootstrap.as -o ./org/windmill/WMBootstrap.swf' + os.system(cmd) + +def clean(): + for root, dirs, file_list in os.walk('./'): + for file in file_list: + if file.endswith('.swf') or file.endswith('.swc') or file.endswith('.swf.cache'): + path = root + '/' + file + cmd = 'rm ' + path + #print cmd + os.system(cmd) + +def parse_opts(): + parser = optparse.OptionParser() + parser.add_option('-t', '--target', dest='target', + help='build TARGET (windmill/bootstrap/all/clean, default is all)', + metavar='TARGET', choices=('windmill', 'bootstrap', 'all', 'clean'), default='all') + opts, args = parser.parse_args() + return opts, args + +def main(o, a): + target = o.target + # Build only the AS tests into loadable swfs + if target == 'windmill': + windmill() + # Build only the test app we use to run the tests against + elif target == 'bootstrap': + bootstrap() + # Build everything, natch + elif target == 'all': + windmill() + bootstrap() + # Clean out any swfs in the directory + elif target == 'clean': + clean() + else: + print 'Not a valid target.' + +if __name__ == "__main__": + main(*parse_opts()) + + diff --git a/org/windmill/TestCase.as b/org/windmill/TestCase.as new file mode 100644 index 0000000..9639895 --- /dev/null +++ b/org/windmill/TestCase.as @@ -0,0 +1,41 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import flash.display.Sprite; + import flash.display.Stage; + import org.windmill.Windmill; + import org.windmill.WMAssert; + import org.windmill.WMWait; + import org.windmill.astest.ASTest; + public class TestCase extends Sprite { + public var windmill:* = Windmill; + public var asserts:* = WMAssert; + public var waits:* = WMWait; + public var controller:* = ASTest.wrappedControllerMethods; + // Reference to either an Application (Flex) + // or the Stage (Flash) + public var context:* = Windmill.getContext(); + // Get a reference to the Stage in the base class + // before the tests actually load so tests can all + // reference it + private var fakeStage:Stage = Windmill.getStage(); + override public function get stage():Stage { + return fakeStage; + } + } +} + diff --git a/org/windmill/WMAssert.as b/org/windmill/WMAssert.as new file mode 100644 index 0000000..184a409 --- /dev/null +++ b/org/windmill/WMAssert.as @@ -0,0 +1,298 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import org.windmill.WMLocator; + import org.windmill.WMLogger; + import flash.utils.getQualifiedClassName; + + public dynamic class WMAssert { + + public static var assertTemplates:Object = { + assertTrue: { + expr: function (a:Boolean):Boolean { + return a === true; + }, + errMsg: 'expected true but was false.' + }, + assertFalse: { + expr: function (a:Boolean):Boolean { + return a === false; + }, + errMsg: 'expected false but was true.' + }, + assertEquals: { + expr: function (a:*, b:*):Boolean { return a === b; }, + errMsg: 'expected $1 but was $2.' + }, + assertNotEquals: { + expr: function (a:*, b:*):Boolean { return a !== b; }, + errMsg: 'expected one of the two values not to be $1.' + }, + assertGreaterThan: { + expr: function (a:*, b:*):Boolean { return a > b; }, + errMsg: 'expected a value greater than $2 but was $1.' + }, + assertLessThan: { + expr: function (a:*, b:*):Boolean { return a < b; }, + errMsg: 'expected a value less than $2 but was $1.' + }, + assertNull: { + expr: function (a:*):Boolean { return a === null; }, + errMsg: 'expected to be null but was $1.' + }, + assertNotNull: { + expr: function (a:*):Boolean { return a !== null; }, + errMsg: 'expected not to be null but was null.' + }, + assertUndefined: { + expr: function (a:*):Boolean { return typeof a == 'undefined'; }, + errMsg: 'expected to be undefined but was $1.' + }, + assertNotUndefined: { + expr: function (a:*):Boolean { return typeof a != 'undefined'; }, + errMsg: 'expected not to be undefined but was undefined.' + }, + assertNaN: { + expr: function (a:*):Boolean { return isNaN(a); }, + errMsg: 'expected $1 to be NaN, but was not NaN.' + }, + assertNotNaN: { + expr: function (a:*):Boolean { return !isNaN(a); }, + errMsg: 'expected $1 not to be NaN, but was NaN.' + }, + assertEvaluatesToTrue: { + expr: function (a:*):Boolean { return !!a; }, + errMsg: 'value of $1 does not evaluate to true.' + }, + assertEvaluatesToFalse: { + expr: function (a:*):Boolean { return !a; }, + errMsg: 'value of $1 does not evaluate to false.' + }, + assertContains: { + expr: function (a:*, b:*):Boolean { + if (typeof a != 'string' || typeof b != 'string') { + throw('Bad argument to assertContains.'); + } + return (a.indexOf(b) > -1); + }, + errMsg: 'value of $1 does not contain $2.' + } + }; + + private static var matchTypes:Object = { + EXACT: 'exact', + CONTAINS: 'contains' + }; + + public static function init():void { + for (var p:String in WMAssert.assertTemplates) { + WMAssert[p] = WMAssert.createAssert(p); + } + } + + private static function createAssert(meth:String):Function { + // Makes sure each assert is called with the right + // number of args + // ------- + var validateArgs:Function = function(count:int, + args:Array):Boolean { + if (!(args.length == count || + (args.length == count + 1 && typeof(args[0]) == 'string') )) { + throw('Incorrect arguments passed to assert function'); + } + return true; + } + // Creates error message for each assert + // ------- + var createErrMsg:Function = function (msg:String, arr:Array):String { + var str:String = msg; + for (var i:int = 0; i < arr.length; i++) { + // When calling jum functions arr is an array with a null entry + if (arr[i] != null){ + var val:* = arr[i]; + var display:String = '<' + val.toString().replace(/\n/g, '') + + '> (' + getQualifiedClassName(val) + ')'; + str = str.replace('$' + (i + 1).toString(), display); + } + } + return str; + } + // Function that runs the dynamically generated asserts + // ------- + var doAssert:Function = function(...args):Boolean { + // The actual assert method, e.g, 'equals' + var meth:String = args.shift(); + // The assert object + var asrt:Object = WMAssert.assertTemplates[meth]; + // The assert expresion + var expr:Function = asrt.expr; + // Validate the args passed + var valid:Boolean = validateArgs(expr.length, args); + // Pull off additional comment which may be first arg + //var comment = args.length > expr.length ? + // args.shift() : null; + // Run the assert + var res:Boolean = expr.apply(null, args); + if (res) { + return true; + } + else { + var message:String = meth + ' -- ' + + createErrMsg(asrt.errMsg, args); + throw new Error(message); + } + } + // Return a function for each dynamically generated + // assert in the assertTemplates list + return function (...args):Boolean { + args.unshift(meth); + return doAssert.apply(null, args); + } + } + + public static function assertDisplayObject(params:Object):Boolean { + var obj:* = WMLocator.lookupDisplayObject(params); + if (!!obj) { + return true; + } + else { + throw new Error('Object ' + obj.toString() + + ' does not exist.'); + } + } + + public static function assertProperty(params:Object):Boolean { + return WMAssert.doBaseAssert(params); + } + + public static function assertText(params:Object):Boolean { + return WMAssert.assertTextGeneric(params, true); + } + + public static function assertTextIn(params:Object):Boolean { + return WMAssert.assertTextGeneric(params, false); + } + + private static function assertTextGeneric(params:Object, + exact:Boolean):Boolean { + return WMAssert.doBaseAssert(params, { + attrName: ['htmlText', 'label'], + preMatchProcess: function (str:String):String { + return str.replace(/^\s*|\s*$/g, ''); + }, + matchType: exact ? WMAssert.matchTypes.EXACT : + WMAssert.matchTypes.CONTAINS + }); + } + + // Workhorse function that does all the main work for + // most asserts + private static function doBaseAssert(params:Object, + opts:Object = null):Boolean { + // Ref to the object to do the lookup on + var obj:* = WMLocator.lookupDisplayObject(params); + // Exact vs. 'in' (contains) match + var matchType:String = WMAssert.matchTypes.EXACT; + var attrName:String; + var expectedVal:String; + // Explicitly passing in the attr name, or a list of + // possible names + if (opts) { + expectedVal = params.validator; + if ('matchType' in opts) { + matchType = opts.matchType; + } + // Passed attr name is a simple string, e.g., + // opts.attrName = 'label' + if (opts.attrName is String) { + attrName = opts.attrName; + attrVal = obj[attrName]; + } + // Passed attr is a list of possible ones + // to look for, in order of priority + // opts.attr = ['htmlText', 'label'] + else if (opts.attrName is Array) { + for each (var item:String in opts.attrName) { + if (item in obj) { + attrName = item; + attrVal = obj[attrName]; + break; + } + } + } + } + // Attr name is passed as part of the validator using + // the pipe syntax: + // foo|bar (Check for attribute foo with value of bar) + // foo.bar.baz|qux (Chained attr lookup -- look for + // bar on foo, then baz on bar, where baz has a + // value of qux. + else { + var validatorStr:String = params.validator; + var validatorArr:Array = validatorStr.split('|'); + if (validatorArr.length != 2) { + throw new Error('validator must have a pipe separator.'); + } + attrName = validatorArr[0]; + expectedVal = validatorArr[1]; + // Attribute may be chained, so loop through any and + // look up the attr we want to check the value on + var attrArr:Array = attrName.split('.'); + var attrVal:Object; + var key:String; + while (attrArr.length) { + if (!attrVal) { + attrVal = obj; + } + key = attrArr.shift(); + if (key in attrVal) { + attrName = key; + attrVal = attrVal[attrName]; + } + else { + throw new Error('"' + key + + '" attribute does not exist on this object.'); + } + } + } + + // Do any preprocessing of the value to check + if (opts.preMatchProcess) { + attrVal = opts.preMatchProcess(attrVal); + } + + // Check for a match + var ret:Boolean = false; + var errMsg:String; + if (matchType == WMAssert.matchTypes.EXACT) { + ret = attrVal == expectedVal; + errMsg = 'Expected "' + expectedVal + '", got "' + attrVal + '"'; + } + else if (matchType == WMAssert.matchTypes.CONTAINS) { + ret = attrVal.indexOf(expectedVal) > -1; + errMsg = '"' + attrVal + '" did not contain "' + expectedVal + '"'; + } + + if (ret) { + return ret; + } + else { + throw new Error(errMsg); + } + } + } +} diff --git a/org/windmill/WMBootstrap.as b/org/windmill/WMBootstrap.as new file mode 100644 index 0000000..1ce0548 --- /dev/null +++ b/org/windmill/WMBootstrap.as @@ -0,0 +1,45 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import flash.display.Loader; + import flash.net.URLRequest; + import flash.events.Event; + import flash.system.ApplicationDomain; + import flash.system.SecurityDomain; + import flash.system.LoaderContext; + + public class WMBootstrap { + public static var windmillLibPath:String = '/flash/org/windmill/Windmill.swf'; + public static var wm:*; + public static function init(context:*, domains:* = null):void { + var loader:Loader = new Loader(); + var url:String = WMBootstrap.windmillLibPath; + var req:URLRequest = new URLRequest(url); + var con:LoaderContext = new LoaderContext(false, + ApplicationDomain.currentDomain, + SecurityDomain.currentDomain); + loader.contentLoaderInfo.addEventListener( + Event.COMPLETE, function ():void { + wm = ApplicationDomain.currentDomain.getDefinition( + "org.windmill.Windmill") as Class; + wm.init({ context: context, domains: domains }); + }); + loader.load(req, con); + } + } +} + diff --git a/org/windmill/WMController.as b/org/windmill/WMController.as new file mode 100644 index 0000000..a56589a --- /dev/null +++ b/org/windmill/WMController.as @@ -0,0 +1,325 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import org.windmill.events.*; + import org.windmill.WMLocator; + import flash.events.* + import mx.events.* + import flash.utils.*; + import flash.geom.Point; + + public class WMController { + public function WMController():void {} + + public static function mouseOver(params:Object):void { + var obj:* = WMLocator.lookupDisplayObject(params); + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_OVER); + Events.triggerMouseEvent(obj, MouseEvent.ROLL_OVER); + } + + public static function mouseOut(params:Object):void { + var obj:* = WMLocator.lookupDisplayObject(params); + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_OUT); + Events.triggerMouseEvent(obj, MouseEvent.ROLL_OUT); + } + + public static function click(params:Object):void { + var obj:* = WMLocator.lookupDisplayObject(params); + // Give it focus + Events.triggerFocusEvent(obj, FocusEvent.FOCUS_IN); + // Down, (TextEvent.LINK,) up, click + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_DOWN, { + buttonDown: true }); + // If this is a link, do the TextEvent hokey-pokey + // All events fire on the containing DisplayObject + if ('link' in params) { + var link:String = WMLocator.locateLinkHref(params.link, + obj.htmlText); + Events.triggerTextEvent(obj, TextEvent.LINK, { + text: link }); + } + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_UP); + Events.triggerMouseEvent(obj, MouseEvent.CLICK); + } + + // Click alias functions + public static function check(params:Object):void { + return WMController.click(params); + } + public static function radio(params:Object):void { + return WMController.click(params); + } + + public static function dragDropElemToElem(params:Object):void { + // Figure out what the destination is + var destParams:Object = {}; + for (var attrib:String in params) { + if (attrib.indexOf('opt') != -1){ + destParams[attrib.replace('opt', '')] = params[attrib]; + break; + } + } + var dest:* = WMLocator.lookupDisplayObject(destParams); + var destCoords:Point = new Point(0, 0); + destCoords = dest.localToGlobal(destCoords); + params.coords = '(' + destCoords.x + ',' + destCoords.y + ')'; + dragDropToCoords(params); + } + + public static function dragDropToCoords(params:Object):void { + var obj:* = WMLocator.lookupDisplayObject(params); + var startCoordsLocal:Point = new Point(0, 0); + var endCoordsAbs:Point = WMController.parseCoords(params.coords); + // Convert local X/Y to global + var startCoordsAbs:Point = obj.localToGlobal(startCoordsLocal); + // Move mouse over to the dragged obj + Events.triggerMouseEvent(obj.stage, MouseEvent.MOUSE_MOVE, { + stageX: startCoordsAbs.x, + stageY: startCoordsAbs.y + }); + Events.triggerMouseEvent(obj, MouseEvent.ROLL_OVER); + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_OVER); + // Give it focus + Events.triggerFocusEvent(obj, FocusEvent.FOCUS_IN); + // Down, (TextEvent.LINK,) up, click + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_DOWN, { + buttonDown: true }); + // Number of steps will be number of pixels in shorter delta + var deltaX:int = endCoordsAbs.x - startCoordsAbs.x; + var deltaY:int = endCoordsAbs.y - startCoordsAbs.y; + var stepCount:int = 10; // Just pick an arbitrary number of steps + // Number of pixels to move per step + var incrX:Number = deltaX / stepCount; + var incrY:Number = deltaY / stepCount; + // Current pos as the move happens + var currXAbs:Number = startCoordsAbs.x; + var currYAbs:Number = startCoordsAbs.y; + var currXLocal:Number = startCoordsLocal.x; + var currYLocal:Number = startCoordsLocal.y; + // Step number + var currStep:int = 0; + // Use a delay so we can see the move + var stepTimer:Timer = new Timer(5); + // Step function -- reposition per step + var doStep:Function = function ():void { + if (currStep <= stepCount) { + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_MOVE, { + stageX: currXAbs, + stageY: currYAbs, + localX: currXLocal, + localY: currYLocal + }); + currXAbs += incrX; + currYAbs += incrY; + currXLocal += incrX; + currYLocal += incrY; + currStep++; + } + // Once it's finished, stop the timer and trigger + // the final mouse events + else { + stepTimer.stop(); + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_UP, { + stageX: currXAbs, + stageY: currYAbs, + localX: currXLocal, + localY: currYLocal + }); + Events.triggerMouseEvent(obj, MouseEvent.CLICK, { + stageX: currXAbs, + stageY: currYAbs, + localX: currXLocal, + localY: currYLocal + }); + } + } + // Start the timer loop + stepTimer.addEventListener(TimerEvent.TIMER, doStep); + stepTimer.start(); + } + + // Ensure coords are in the right format and are numbers + private static function parseCoords(coordsStr:String):Point { + var coords:Array = coordsStr.replace( + /\(|\)| /g, '').split(','); + var point:Point; + if (isNaN(coords[0]) || isNaN(coords[1])) { + throw new Error('Coordinates must be in format "(x, y)"'); + } + else { + coords[0] = parseInt(coords[0], 10); + coords[1] = parseInt(coords[1], 10); + point = new Point(coords[0], coords[1]); + } + return point; + } + + public static function doubleClick(params:Object):void { + var obj:* = WMLocator.lookupDisplayObject(params); + // Give it focus + Events.triggerFocusEvent(obj, FocusEvent.FOCUS_IN); + // First click + // Down, (TextEvent.LINK,) up, click + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_DOWN, { + buttonDown: true }); + // If this is a link, do the TextEvent hokey-pokey + // All events fire on the containing DisplayObject + if ('link' in params) { + var link:String = WMLocator.locateLinkHref(params.link, + obj.htmlText); + Events.triggerTextEvent(obj, TextEvent.LINK, { + text: link }); + } + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_UP); + Events.triggerMouseEvent(obj, MouseEvent.CLICK); + // Second click + // Down, (TextEvent.LINK,) up, double click + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_DOWN, { + buttonDown: true }); + // TextEvent hokey-pokey, reprise + if ('link' in params) { + Events.triggerTextEvent(obj, TextEvent.LINK, { + text: link }); + } + Events.triggerMouseEvent(obj, MouseEvent.MOUSE_UP); + Events.triggerMouseEvent(obj, MouseEvent.DOUBLE_CLICK); + } + + public static function type(params:Object):void { + // Look up the item to write to + var obj:* = WMLocator.lookupDisplayObject(params); + // Text to type out + var str:String = params.text; + // Char + var currChar:String; + // Char code + var currCode:int; + + // Give the item focus + Events.triggerFocusEvent(obj, FocusEvent.FOCUS_IN); + // Clear out any value it previously had + obj.text = ''; + + // Write out the string, firing appropriate events as you go + for (var i:int = 0; i < str.length; i++) { + currChar = str.charAt(i); + currCode = str.charCodeAt(i); + // FIXME: In reality, capital letters / special chars + // would be firing shift key events around these + Events.triggerKeyboardEvent(obj, KeyboardEvent.KEY_DOWN, { + charCode: currCode }); + // Append to the value + obj.text += str.charAt(i); + Events.triggerTextEvent(obj, TextEvent.TEXT_INPUT, { + text: currChar }); + Events.triggerKeyboardEvent(obj, KeyboardEvent.KEY_UP, { + charCode: currCode }); + } + } + + public static function select(params:Object):void { + // Look up the item to write to + var obj:* = WMLocator.lookupDisplayObject(params); + var sel:* = obj.selectedItem; + var item:*; + // Give the item focus + Events.triggerFocusEvent(obj, FocusEvent.FOCUS_IN); + // Set by index + switch (true) { + case ('index' in params): + if (obj.selectedIndex != params.index) { + Events.triggerListEvent(obj, ListEvent.CHANGE); + obj.selectedIndex = params.index; + } + break; + case ('label' in params): + case ('text' in params): + var targetLabel:String = params.label || params.text; + // Can set a custom label field via labelField attr + var labelField:String = obj.labelField ? + obj.labelField : 'label'; + if (sel[labelField] != targetLabel) { + Events.triggerListEvent(obj, ListEvent.CHANGE); + for each (item in obj.dataProvider) { + if (item[labelField] == targetLabel) { + obj.selectedItem = item; + } + } + } + break; + case ('data' in params): + case ('value' in params): + var targetData:String = params.data || params.value; + if (sel.data != targetData) { + Events.triggerListEvent(obj, ListEvent.CHANGE); + for each (item in obj.dataProvider) { + if (item.data == targetData) { + obj.selectedItem = item; + } + } + } + break; + default: + // Do nothing + } + } + public static function getTextValue(params:Object):String { + // Look up the item where we want to get the property + var obj:* = WMLocator.lookupDisplayObject(params); + var attrs:Object=['htmlText', 'label']; + var res:String = 'undefined'; + var attr:String; + for each (attr in attrs){ + res = obj[attr]; + if (res != 'undefined'){ + break; + } + } + return res; + } + + public static function getPropertyValue(params:Object, opts:Object = null):String { + // Look up the item where we want to get the property + var obj:* = WMLocator.lookupDisplayObject(params); + var attrName:String; + var attrVal:String = 'undefined'; + if (opts){ + if (opts.attrName is String) { + attrName = opts.attrName; + attrVal = obj[attrName]; + } + } + else { + if (params.attrName is String) { + attrName = params.attrName; + attrVal = obj[attrName]; + } + } + return String(attrVal); + } + + public static function getObjectCoords(params:Object):String { + // Look up the item which coords we want to get + var obj:* = WMLocator.lookupDisplayObject(params); + var destCoords:Point = new Point(0, 0); + destCoords = obj.localToGlobal(destCoords); + var coords:String = '(' + String(destCoords.x) + ',' + String(destCoords.y) + ')'; + return coords; + } + } +} + diff --git a/org/windmill/WMExplorer.as b/org/windmill/WMExplorer.as new file mode 100644 index 0000000..0781da1 --- /dev/null +++ b/org/windmill/WMExplorer.as @@ -0,0 +1,131 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import org.windmill.Windmill; + import org.windmill.WMLocator; + import org.windmill.WMLogger; + import org.windmill.WMRecorder; + import flash.display.Stage; + import flash.display.Sprite; + import flash.events.MouseEvent; + import flash.geom.Point; + import flash.geom.Rectangle; + import flash.external.ExternalInterface; + + public class WMExplorer { + // Sprite which gets superimposed on the moused-over element + // and provides the border effect + private static var borderSprite:Sprite = new Sprite(); + private static var running:Boolean = false; + private static var strictLocators:* = null; // true, false, or null + + public static function init():void { + } + + public static function start(...args):void { + if (args.length) { + strictLocators = args[0]; + } + else { + strictLocators = null; + } + + // Stop the recorder if it's going + WMRecorder.stop(); + running = true; + + var stage:Stage = Windmill.getStage(); + var spr:Sprite = borderSprite; + // Add the border-sprite to the stage + spr.name = 'windmillBorderSprite'; + stage.addChild(spr); + // Highlight every element, create locator chain on mouseover + stage.addEventListener(MouseEvent.MOUSE_OVER, select, false); + // Stop on click + stage.addEventListener(MouseEvent.MOUSE_DOWN, annihilateEvent, true); + stage.addEventListener(MouseEvent.MOUSE_UP, annihilateEvent, true); + // This passes off to annihilateEvent to kill clicks too + stage.addEventListener(MouseEvent.CLICK, stop, true); + } + + public static function stop(e:MouseEvent = null):void { + if (!running) { return; } + var stage:Stage = Windmill.getStage(); + + stage.removeChild(borderSprite); + stage.removeEventListener(MouseEvent.MOUSE_OVER, select); + // Call removeEventListener with useCapture of 'true', since + // the listener was added with true + stage.removeEventListener(MouseEvent.MOUSE_DOWN, annihilateEvent, true); + stage.removeEventListener(MouseEvent.MOUSE_UP, annihilateEvent, true); + stage.removeEventListener(MouseEvent.CLICK, stop, true); + running = false; + // Pass off to annihilateEvent to prevent the app from responding + annihilateEvent(e); + + var res:* = ExternalInterface.call('wm_explorerStopped'); + if (!res) { + WMLogger.log('(Windmill Flash bridge not found.)'); + } + } + + // Highlights the rolled-over item and generates a chained-locator + // expression for it + public static function select(e:MouseEvent):void { + var targ:* = e.target; + if ('name' in targ && targ.name == 'windmillBorderSprite') { + return; + } + // Bordered sprite for highlighting + var spr:Sprite = borderSprite; + // Get the global coords of the moused-over elem + // Overlay the border sprite in the same position + var bounds:Rectangle = targ.getBounds(targ.parent); + var p:Point = new Point(bounds.x, bounds.y); + p = targ.parent.localToGlobal(p); + spr.x = p.x; + spr.y = p.y; + // Clear any previous border, and draw a new border + // the same size as the moused-over elem + spr.graphics.clear() + spr.graphics.lineStyle(2, 0x3875d7, 1); + spr.graphics.drawRect(0, 0, targ.width, targ.height); + // Generate the expression + var args:Array = []; + args.push(targ); + if (strictLocators is Boolean) { + args.push(strictLocators); + } + var expr:String = WMLocator.generateLocator.apply(WMLocator, args); + if (expr && expr.length) { + var res:* = ExternalInterface.call('wm_explorerSelect', expr); + if (!res) { + WMLogger.log('Locator chain: ' + expr); + } + } + else { + throw new Error('Could not find any usable attributes for locator.'); + } + } + + public static function annihilateEvent(e:MouseEvent):void { + e.preventDefault(); + e.stopImmediatePropagation(); + } + + } +} diff --git a/org/windmill/WMLocator.as b/org/windmill/WMLocator.as new file mode 100644 index 0000000..656988d --- /dev/null +++ b/org/windmill/WMLocator.as @@ -0,0 +1,335 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import org.windmill.Windmill; + import org.windmill.WMLogger; + import flash.display.DisplayObject; + import flash.display.DisplayObjectContainer; + + public class WMLocator { + // Stupid AS3 doesn't iterate over Object keys + // in insertion order + // null for the finder func means use the default + // of findBySimpleAttr + private static var locatorMap:Array = [ + ['name', null], + ['id', null], + ['link', WMLocator.findLink], + ['label', null], + ['htmlText', WMLocator.findHTML], + ['automationName', null] + ]; + private static var locatorMapObj:Object = {}; + private static var locatorMapCreated:Boolean = false; + + // This is the list of attrs we like to use for the + // locators, in order of preference + // FIXME: Need to add some regex fu for pawing through + // text containers for Flash's janky anchor-tag impl + private static var locatorLookupPriority:Array = [ + 'automationName', + 'id', + 'name', + 'label', + 'htmlText' + ]; + + public static function init():void { + for each (var arr:Array in WMLocator.locatorMap) { + WMLocator.locatorMapObj[arr[0]] = arr[1]; + } + WMLocator.locatorMapCreated = true; + } + + public static function lookupDisplayObjectBool( + params:Object):Boolean { + + var res:DisplayObject; + res = WMLocator.lookupDisplayObject(params); + if (res){ + return true; + } + return false; + } + + public static function lookupDisplayObject( + params:Object):DisplayObject { + var res:DisplayObject; + res = lookupDisplayObjectForContext(params, Windmill.getContext()); + if (!res && Windmill.contextIsApplication()) { + res = lookupDisplayObjectForContext(params, Windmill.getStage()); + } + + return res; + } + + public static function lookupDisplayObjectForContext( + params:Object, obj:*):DisplayObject { + var locators:Array = []; + var queue:Array = []; + var checkWMLocatorChain:Function = function ( + item:*, pos:int):DisplayObject { + var map:Object = WMLocator.locatorMapObj; + var loc:Object = locators[pos]; + // If nothing specific exists for that attr, use the basic one + var finder:Function = map[loc.attr] || WMLocator.findBySimpleAttr; + var next:int = pos + 1; + if (!!finder(item, loc.attr, loc.val)) { + // Move to the next locator in the chain + // If it's the end of the chain, we have a winner + if (next == locators.length) { + return item; + } + // Otherwise recursively check the next link in + // the locator chain + var count:int = 0; + if (item is DisplayObjectContainer) { + count = item.numChildren; + } + if (count > 0) { + var index:int = 0; + while (index < count) { + var kid:DisplayObject = item.getChildAt(index); + var res:DisplayObject = checkWMLocatorChain(kid, next); + if (res) { + return res; + } + index++; + } + } + } + return null; + }; + var str:String = normalizeWMLocator(params); + locators = parseWMLocatorChainExpresson(str); + queue.push(obj); + while (queue.length) { + // Otherwise grab the next item in the queue + var item:* = queue.shift(); + // Append any kids to the end of the queue + if (item is DisplayObjectContainer) { + var count:int = item.numChildren; + var index:int = 0; + while (index < count) { + var kid:DisplayObject = item.getChildAt(index); + queue.push(kid); + index++; + } + } + var res:DisplayObject = checkWMLocatorChain(item, 0); + // If this is a full match, we're done + if (res) { + return res; + } + } + return null; + } + + private static function parseWMLocatorChainExpresson( + exprStr:String):Array { + var locators:Array = []; + var expr:Array = exprStr.split('/'); + var arr:Array; + for each (var item:String in expr) { + arr = item.split(':'); + locators.push({ + attr: arr[0], + val: arr[1] + }); + } + return locators; + } + + private static function normalizeWMLocator(params:Object):String { + if ('chain' in params) { + return params.chain; + } + else { + var map:Object = WMLocator.locatorMap; + var attr:String; + var val:*; + // WMLocators have an order of precedence -- ComboBox will + // have a name/id, and its sub-options will have label + // Make sure to do name-/id-based lookups first, label last + for each (var item:Array in map) { + if (item[0] in params) { + attr = item[0]; + val = params[attr]; + break; + } + } + return attr + ':' + val; + } + } + + // Default locator for all basic key/val attr matches + private static function findBySimpleAttr( + obj:*, attr:String, val:*):Boolean { + return !!(attr in obj && obj[attr] == val); + } + + // Custom locator for links embedded in htmlText + private static function findLink( + obj:*, attr:String, val:*):Boolean { + var res:Boolean = false; + if ('htmlText' in obj) { + res = !!locateLinkHref(val, obj.htmlText); + } + return res; + } + + // Custom locator for links embedded in htmlText + private static function findHTML( + obj:*, attr:String, val:*):Boolean { + var res:Boolean = false; + if ('htmlText' in obj) { + var text:String = WMLocator.cleanHTML(obj.htmlText); + return val == text; + } + return res; + } + + // Used by the custom locator for links, above + public static function locateLinkHref(linkText:String, + htmlText:String):String { + var pat:RegExp = /()([\s\S]*?)(?:<\/a>)/gi; + var res:Array; + var linkPlain:String = ''; + while (!!(res = pat.exec(htmlText))) { + // Remove HTML tags and linebreaks; and trim + linkPlain = WMLocator.cleanHTML(res[2]); + if (linkPlain == linkText) { + var evPat:RegExp = /href="event:(.*?)"/i; + var arr:Array = evPat.exec(res[1]); + if (!!(arr && arr[1])) { + return arr[1]; + } + else { + return ''; + } + } + } + return ''; + } + + private static function cleanHTML(markup:String):String { + return markup.replace(/<.+?>/g, '').replace( + /\s+/g, ' ').replace(/^ | $/g, ''); + } + + // Generates a chained-locator expression for the clicked-on item + public static function generateLocator(item:*, ...args):String { + var strictLocators:Boolean = Windmill.config.strictLocators; + if (args.length) { + strictLocators = args[0]; + } + var expr:String = ''; + var exprArr:Array = []; + var attr:String; + var attrVal:String; + // Verifies the property exists, and that the child can + // be found from the parent (in some cases there is a parent + // which does not have the item in its list of children) + var weHaveAWinner:Function = function (item:*, attr:String):Boolean { + var winner:Boolean = false; + // Get an attribute that actually has a value + if (usableAttr(item, attr)) { + // Make sure that the parent can actually see + // this item in its list of children + var par:* = item.parent; + var count:int = 0; + if (par is DisplayObjectContainer) { + count = par.numChildren; + } + if (count > 0) { + var index:int = 0; + while (index < count) { + var kid:DisplayObject = par.getChildAt(index); + if (kid == item) { + winner = true; + break; + } + index++; + } + } + } + return winner; + }; + var usableAttr:Function = function (item:*, attr:String):Boolean { + // Item has to have an attribute of that name + if (!(attr in item)) { + return false; + } + // Attribute's value cannot be null + if (!item[attr]) { + return false; + } + // If strict locators are on, don't accept an auto-generated + // 'name' attribute ending in a number -- e.g., TextField05 + // These are often unreliable as locators + if (strictLocators && + attr == 'name' && /\d+$/.test(item[attr])) { + return false; + } + return true; + }; + var isValidLookup:Function = function (exprArr:Array):Boolean { + expr = exprArr.join('/'); + // Make sure that the expression actually looks up a + // valid object + var validLookup:DisplayObject = lookupDisplayObject({ + chain: expr + }); + return !!validLookup; + }; + // Attrs to look for, ordered by priority + var locatorPriority:Array = WMLocator.locatorLookupPriority; + do { + // Try looking up a value for each attribute in order + // of preference + for each (attr in locatorPriority) { + // If we find one of the lookuup keys, we may have a winner + if (weHaveAWinner(item, attr)) { + // Prepend onto the locator expression, then check to + // see if the chain still results in a valid lookup + attrVal = attr == 'htmlText' ? + WMLocator.cleanHTML(item[attr]) : item[attr]; + exprArr.unshift(attr + ':' + attrVal); + // If this chain looks up an object correct, keeps going + if (isValidLookup(exprArr)) { + break; + } + // Otherwise throw out this attr/value pair and keep + // trying + else { + exprArr.shift(); + } + } + } + item = item.parent; + } while (item.parent && !(item.parent == Windmill.getContext() || + item.parent == Windmill.getStage())) + if (exprArr.length) { + expr = exprArr.join('/'); + return expr; + } + else { + return null; + } + } + } +} diff --git a/org/windmill/WMLogger.as b/org/windmill/WMLogger.as new file mode 100644 index 0000000..dbc8b22 --- /dev/null +++ b/org/windmill/WMLogger.as @@ -0,0 +1,35 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import flash.external.ExternalInterface; + + public class WMLogger { + public static var modes:Object = { + TRACE: 'trace', + BROWSER: 'browser' + }; + public static var mode:String = modes.BROWSER; + public static function log(msg:*):void { + if (WMLogger.mode == modes.BROWSER) { + ExternalInterface.call("console.log", msg); + } + else { + trace(msg.toString()); + } + } + } +} diff --git a/org/windmill/WMRecorder.as b/org/windmill/WMRecorder.as new file mode 100644 index 0000000..d86faca --- /dev/null +++ b/org/windmill/WMRecorder.as @@ -0,0 +1,216 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import org.windmill.Windmill; + import org.windmill.WMLogger; + import org.windmill.WMLocator; + import org.windmill.WMExplorer; + import flash.utils.*; + import flash.display.Stage; + import flash.display.DisplayObject; + import flash.display.DisplayObjectContainer; + import flash.events.MouseEvent; + import flash.events.TextEvent; + import flash.events.KeyboardEvent; + import mx.events.ListEvent; + import mx.controls.ComboBox; + import mx.controls.List; + import flash.external.ExternalInterface; + + public class WMRecorder { + // Remember the last event type so we know when to + // output the stored string from a sequence of keyDown events + private static var lastEventType:String; + // Remember recent target -- used to detect double-click + // and to throw away click events on text items that have + // already spawned a 'link' TextEvent. + // Only remembered for one second + private static var recentTarget:Object = { + click: null, + change: null + }; + // Timeout id for removing the recentTarget + private static var recentTargetTimeout:Object = { + click: null, + change: null + }; + // String built from a sequenece of keyDown events + private static var keyDownString:String = ''; + private static var listItems:Array = []; + private static var running:Boolean = false; + + public function WMRecorder():void {} + + public static function start():void { + // Stop the explorer if it's going + WMExplorer.stop(); + + var recurseAttach:Function = function (item:*):void { + // Otherwise recursively check the next link in + // the locator chain + var count:int = 0; + if (item is ComboBox || item is List) { + WMRecorder.listItems.push(item); + item.addEventListener(ListEvent.CHANGE, WMRecorder.handleEvent); + } + if (item is DisplayObjectContainer) { + count = item.numChildren; + } + if (count > 0) { + var index:int = 0; + while (index < count) { + var kid:DisplayObject = item.getChildAt(index); + var res:DisplayObject = recurseAttach(kid); + index++; + } + } + } + recurseAttach(Windmill.getContext()); + var stage:Stage = Windmill.getStage(); + stage.addEventListener(MouseEvent.CLICK, WMRecorder.handleEvent); + stage.addEventListener(MouseEvent.DOUBLE_CLICK, WMRecorder.handleEvent); + stage.addEventListener(TextEvent.LINK, WMRecorder.handleEvent); + stage.addEventListener(KeyboardEvent.KEY_DOWN, WMRecorder.handleEvent); + + WMRecorder.running = true; + } + + public static function stop():void { + if (!WMRecorder.running) { return; } + var stage:Stage = Windmill.getStage(); + stage.removeEventListener(MouseEvent.CLICK, WMRecorder.handleEvent); + stage.removeEventListener(MouseEvent.DOUBLE_CLICK, WMRecorder.handleEvent); + stage.removeEventListener(TextEvent.LINK, WMRecorder.handleEvent); + stage.removeEventListener(KeyboardEvent.KEY_DOWN, WMRecorder.handleEvent); + var list:Array = WMRecorder.listItems; + for each (var item:* in list) { + item.removeEventListener(ListEvent.CHANGE, WMRecorder.handleEvent); + } + } + + private static function handleEvent(e:*):void { + var targ:* = e.target; + var _this:* = WMRecorder; + var chain:String = WMLocator.generateLocator(targ); + + switch (e.type) { + // Keyboard input -- append to the stored string reference + case KeyboardEvent.KEY_DOWN: + _this.keyDownString += String.fromCharCode(e.charCode); + break; + // ComboBox changes + case ListEvent.CHANGE: + _this.generateAction('select', targ); + _this.resetRecentTarget('change', e); + break; + // Mouse/URL clicks + default: + // If the last event was a keyDown, write out the string + // that's been saved from the sequence of keyboard events + if (_this.lastEventType == KeyboardEvent.KEY_DOWN) { + _this.generateAction('type', targ, { text: _this.keyDownString }); + // Empty out string storage + _this.keyDownString = ''; + } + // Ignore clicks on ComboBox/List items that result + // in ListEvent.CHANGE events -- the list gets blown + // away, and can't be looked up by the generated locator + // anyway, so we have to use this event instead + else if (_this.lastEventType == ListEvent.CHANGE) { + if (_this.recentTarget.change) { + return; + } + } + // Avoid multiple clicks on the same target + if (_this.recentTarget == e.target) { + // Check for previous TextEvent.LINK + if (_this.lastEventType != MouseEvent.DOUBLE_CLICK) { + // Just throw this mofo away + return; + } + } + var t:String = e.type == MouseEvent.DOUBLE_CLICK ? + 'doubleClick' : 'click'; + _this.generateAction(t, targ); + _this.resetRecentTarget('click', e); + } + + // Remember the last event type for saving sequences of + // keyboard events + _this.lastEventType = e.type; + + //WMLogger.log(e.toString()); + //WMLogger.log(e.target.toString()); + } + + private static function resetRecentTarget(t:String, e:*):void { + var _this:* = WMRecorder; + // Remember this target, avoid multiple clicks on it + _this.recentTarget[t] = e.target; + // Cancel any old setTimeout still hanging around + if (_this.recentTargetTimeout[t]) { + clearTimeout(_this.recentTargetTimeout[t]); + } + // Clear the recentTarget after 1 sec. + _this.recentTargetTimeout[t] = setTimeout(function ():void { + _this.recentTarget[t] = null; + _this.recentTargetTimeout[t] = null; + }, 1); + } + + private static function generateAction(t:String, targ:*, + opts:Object = null):void { + var chain:String = WMLocator.generateLocator(targ); + var res:Object = { + method: t, + chain: chain + }; + var params:Object = {}; + var p:String; + for (p in opts) { + params[p] = opts[p] + } + switch (t) { + case 'click': + break; + case 'doubleClick': + break; + case 'select': + var sel:* = targ.selectedItem; + // Can set a custom label field via labelField attr + var labelField:String = targ.labelField ? + targ.labelField : 'label'; + params.label = sel[labelField]; + break; + case 'type': + break; + } + for (p in params) { + res.params = params; + break; + } + + var r:* = ExternalInterface.call('wm_recorderAction', res); + if (!r) { + WMLogger.log(res); + WMLogger.log('(Windmill Flash bridge not found.)'); + } + } + + } +} + diff --git a/org/windmill/WMWait.as b/org/windmill/WMWait.as new file mode 100644 index 0000000..848c377 --- /dev/null +++ b/org/windmill/WMWait.as @@ -0,0 +1,122 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import org.windmill.Windmill; + import org.windmill.WMLogger; + import org.windmill.WMLocator; + import org.windmill.astest.ASTest; + import flash.utils.*; + + public class WMWait { + // Simple wait function -- puts ASTest into waiting + // mode, calls a function on setTimeout to take it + // back out of waiting mode + public static function sleep(params:Object):void { + ASTest.waiting = true; + setTimeout(function ():void { + ASTest.waiting = false; + }, params.milliseconds); + } + + // Generic wait function which waits for a true result + // from a test function (params.test) + // All other waits should simply define a test function + // and hand off to this + // Default timeout (Windmill.config.timeout) is 20 seconds -- + // can be overridden with params.timeout + public static function forCondition(params:Object, + callback:Function = null):void { + var timeout:int = Windmill.config.timeout; + if (params.timeout) { + if (!isNaN(parseInt(params.timeout, 10))) { + timeout = params.timeout; + } + } + var testFunc:Function = params.test; + var timeoutCounter:int = 0; + var loopInterval:int = 100; + + ASTest.waiting = true; + + // Recursively call the test function, and set + // ASTest.waiting back to false if the code ever suceeds + // Throw an error if this loop times out without + // the test function ever succeeding + var conditionTest:Function = function ():void { + + // If test function never returns a true result, time out. + // Can't throw an actual error here, because after the first + // setTimeout, this recursive call-loop executes outside the + // scope of the original try/catch in the ASTest.runNextTest + // loop. So rather than throwing here, we hang the error on + // ASTest.previousError, so when runNextTest resumes, it will + // find it and report it before running the next test action + if (timeoutCounter > timeout) { + ASTest.previousError = new Error( + 'Wait timed out after ' + timeout + ' milliseconds.'); + ASTest.waiting = false; + return; + } + + // Not timed out, so increment the counter and go on + timeoutCounter += loopInterval; + + // Exec the test function, and cast it to a Bool + var result:*; + try { + result = testFunc(); + } + // If it throws an error, just try again -- if it never + // succeeds, the timeout code will handle it + catch (e:Error) { + return; + } + result = !!result; + + // Success -- switch off waiting state so ASTest.runNextTest + // will resume + if (result) { + if (callback is Function) { + try { + callback(); + } + catch (e:Error) { + ASTest.previousError = e; + } + } + ASTest.waiting = false; + return; + } + // Otherwise keep trying until it times out + else { + setTimeout(conditionTest, loopInterval); + } + }; + conditionTest(); // Start the recursive calling process + } + + public static function forDisplayObject(params:Object, + callback:Function = null):void { + var func:Function = function ():Boolean { + var obj:* = WMLocator.lookupDisplayObject(params); + return !!obj + } + params.test = func; + return WMWait.forCondition(params, callback); + } + } +} diff --git a/org/windmill/Windmill.as b/org/windmill/Windmill.as new file mode 100644 index 0000000..5de9ae7 --- /dev/null +++ b/org/windmill/Windmill.as @@ -0,0 +1,196 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill { + import org.windmill.astest.ASTest; + import org.windmill.WMLocator; + import org.windmill.WMController; + import org.windmill.WMAssert; + import flash.utils.*; + import mx.core.Application; + import flash.system.Security; + import flash.display.Sprite; + import flash.display.Stage; + import flash.display.DisplayObject; + import flash.external.ExternalInterface; + + public class Windmill extends Sprite { + public static var config:Object = { + context: null, // Ref to the Stage or Application + timeout: 20000, // Default timeout for waits + domains: [], + strictLocators: false + }; + public static var controllerMethods:Array = []; + public static var assertMethods:Array = []; + public static var packages:Object = { + controller: { + // Ref to the namespace, since you can't + // do it via string lookup + packageRef: org.windmill.WMController, + // Gets filled with the list of public methods -- + // used to generate the wrapped methods exposed + // via ExternalInterface + methodNames: [] + }, + assert: { + packageRef: org.windmill.WMAssert, + methodNames: [] + } + }; + + // Initializes the Windmill Flash code + // 1. Saves a reference to the stage in config.context + // this is the equivalent of the window obj in + // Windmill's JS impl. See WMLocator to see how + // it's used + // 2. Does some introspection/metaprogramming to + // expose all the public methods in WMController + // and WMAsserts through the ExternalInterface + // as wrapped functions that return either the + // Boolean true, or the Error object if an error + // happens (as in the case of all failed tests) + // 3. Exposes the start/stop method of WMExplorer + // to turn on and off the explorer + public static function init(params:Object):void { + var methodName:String; + var item:*; + var descr:XML; + // A reference to the Stage + // ---------------- + if (!(params.context is Stage || params.context is Application)) { + throw new Error('Windmill.config.context must be a reference to the Application or Stage.'); + } + config.context = params.context; + + // Allow script access to talk to the Windmill API + // via ExternalInterface from the following domains + if ('domains' in params) { + var domainsArr:Array = params.domain is Array ? + params.domains : [params.domains]; + config.domains = domainsArr; + for each (var d:String in config.domains) { + Windmill.addDomain(d); + } + } + + // Set up the locator map + // ======== + WMLocator.init(); + // Create dynamic asserts + // ======== + WMAssert.init(); + + // Returns a wrapped version of the method that returns + // the Error obj to JS-land instead of actually throwing + var genExtFunc:Function = function (func:Function):Function { + return function (...args):* { + try { + return func.apply(null, args); + } + catch (e:Error) { + return e; + } + } + } + + // Expose controller and non-dynamic assert methods + // ---------------- + for (var key:String in packages) { + // Introspect all the public packages + // to expose via ExternalInterface + descr = flash.utils.describeType( + packages[key].packageRef); + for each (item in descr..method) { + packages[key].methodNames.push(item.@name.toXMLString()); + } + // Expose public packages via ExternalInterface + // 'dragDropOnCoords' becomes 'wm_dragDropOnCoords' + // The exposed method is wrapped in a try/catch + // that returns the Error obj to JS instead of throwing + for each (methodName in packages[key].methodNames) { + ExternalInterface.addCallback('wm_' + methodName, + genExtFunc(packages[key].packageRef[methodName])); + } + } + + // Expose dynamic asserts + // ---------------- + // These *will not* + // show up via introspection with describeType, but + // they *are there* -- add them manually by iterating + // through the same list that used to build them + var asserts:* = WMAssert; + for (methodName in asserts.assertTemplates) { + ExternalInterface.addCallback('wm_' + methodName, + genExtFunc(asserts[methodName])); + + } + + // Other misc ExternalInterface methods + // ---------------- + var miscMethods:Object = { + explorerStart: WMExplorer.start, + explorerStop: WMExplorer.stop, + recorderStart: WMRecorder.start, + recorderStop: WMRecorder.stop, + runASTests: ASTest.run, + lookupFlash: WMLocator.lookupDisplayObjectBool + } + for (methodName in miscMethods) { + ExternalInterface.addCallback('wm_' + methodName, + genExtFunc(miscMethods[methodName])); + } + + // Wrap controller methods for AS tests to do auto-wait + // ======== + ASTest.init(); + } + + public static function addDomain(domain:String):void { + flash.system.Security.allowDomain(domain); + } + + public static function contextIsStage():Boolean { + return (config.context is Stage); + } + + public static function contextIsApplication():Boolean { + return (config.context is Application); + } + + public static function getContext():* { + return config.context; + } + + public static function getStage():Stage { + var context:* = config.context; + var stage:Stage; + if (contextIsApplication()) { + stage = context.stage; + } + else if (contextIsStage()) { + stage = context; + } + else { + throw new Error('Windmill.config.context must be a reference to an Application or Stage.' + + ' Perhaps Windmill.init has not run yet.'); + } + return stage; + } + + } +} diff --git a/org/windmill/astest/ASTest.as b/org/windmill/astest/ASTest.as new file mode 100644 index 0000000..d97eda4 --- /dev/null +++ b/org/windmill/astest/ASTest.as @@ -0,0 +1,301 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.astest { + import org.windmill.Windmill; + import org.windmill.WMLocator; + import org.windmill.WMController; + import org.windmill.WMWait; + import org.windmill.WMLogger; + import flash.utils.*; + import flash.external.ExternalInterface; + + public class ASTest { + // How long to wait between each test action + private static const TEST_INTERVAL:int = 10; + // List of all the test classes for this test run + public static var testClassList:Array = []; + // The complete list of all methods for each class + // in this test run + private static var testListComplete:Array = []; + // Copy of the list of tests -- items are popped + // of to run the tests + private static var testList:Array = []; + // The last test action -- used to do reporting on + // success/failure of each test. Waits happen + // async in a setTimeout loop, so reporting happens + // for the *previous* test at the beginning of each + // runNextTest call, before grabbing and running the + // next test + private static var previousTest:Object = null; + // Error for the previous test if it was unsuccessful + // Used in the reporting as described above + public static var previousError:Object = false; + // Tests are running or not + public static var inProgress:Boolean = false; + // In waiting mode, the runNextTest loop just idles + public static var waiting:Boolean = false; + + public static var wrappedControllerMethods:Object = {}; + + public static function init():void { + var methodNames:Array = Windmill.packages.controller.methodNames; + // Returns a controller action wrapped in a wait for the + // desired DisplayObject -- action is passed as a callback + // to WMWait.forDisplayObject + var wrapAutoWait:Function = function (key:String):Function { + return function (params:Object):void { + WMWait.forDisplayObject(params, function ():void { + WMController[key](params); + }); + } + } + // For each controller-action method in WMController, + // create an auto-wait-wrapped version to call from the + // AS tests. 'controller' in the TestCase base class + // points to wrappedControllerMethods + for each (var key:String in methodNames) { + wrappedControllerMethods[key] = wrapAutoWait(key); + } + } + + public static function run(files:* = null):void { + //['/flash/TestFoo.swf', '/flash/TestBar.swf'] + // If we're passed some files, load 'em up first + // the loader will call back to this again when + // it's done, with no args + if (files) { + // **** Ugly hack **** + // ------------- + if (!(files is Array)) { + // The files param passed in from XPCOM trusted JS + // loses its Array-ness -- fails the 'is Array' test, + // and has no 'length' property. It's just a generic + // Object with integers for keys + // In that case, reconstitute the Array by manually + // stepping through it until we run out of items + var filesTemp:Array = []; + var incr:int = 0; + var item:*; + var keepGoing:Boolean = true; + while (keepGoing) { + item = files[incr]; + if (item) { + filesTemp.push(item); + } + else { + keepGoing = false; + } + incr++; + } + files = filesTemp; + } + // ------------- + ASTest.loadTestFiles(files); + return; + } + ASTest.getCompleteListOfTests(); + ASTest.start(); + } + + public static function loadTestFiles(files:Array):void { + // Clear out the list of tests before loading + ASTest.testClassList = []; + ASTest.testList = []; + // Load the shit + ASTestLoader.load(files); + } + + public static function start():void { + // Make a copy of the tests to work on + ASTest.testList = ASTest.testListComplete.slice(); + ASTest.inProgress = true; + // Run recursively in a setTimeout loop so + // we can implement sleeps and waits + ASTest.runNextTest(); + } + + public static function runNextTest():void { + var test:Object = null; + var res:*; // Result from ExternalInterface calls + var data:Object; + // If we're idling in a wait, just move along ... + // Nothing to see here + if (ASTest.waiting) { + // Let's try again in a second or so + setTimeout(function ():void { + ASTest.runNextTest.call(ASTest); + }, 1000); + return; + } + // Do reporting for the previous test -- we do this here + // because waits happen async in a setTimeout loop, + // and we only know when it has finished by when the next + // test actually starts + if (ASTest.previousTest) { + test = ASTest.previousTest; + data = { + test: { + className: test.className, + methodName: test.methodName + }, + error: null + }; + // Error + if (ASTest.previousError) { + data.error = ASTest.previousError; + ASTest.previousError = null; + } + + // Report via ExternalInterface, or log results + res = ExternalInterface.call('wm_asTestResult', data); + if (!res) { + if (data.error) { + WMLogger.log('FAILURE: ' + data.error.message); + } + else { + WMLogger.log('SUCCESS'); + } + } + ASTest.previousTest = null; + } + + // If we're out of tests, we're all done + // TODO: Add some kind of final report + if (ASTest.testList.length == 0) { + ASTest.inProgress = false; + } + // If we still have tests to run, grab the next one + // and run that bitch + else { + test = ASTest.testList.shift(); + // Save a ref to this test to use for reporting + // at the beginning of the next call + ASTest.previousTest = test; + + data = { + test: { + className: test.className, + methodName: test.methodName + } + }; + res = ExternalInterface.call('wm_asTestStart', data); + if (!res) { + WMLogger.log('Running ' + test.className + '.' + test.methodName + ' ...'); + } + + // Run the test + // ----------- + try { + if (!(test.methodName in test.instance)) { + throw new Error('"' + test.methodName + + '" is not a valid method in' + test.instance.toString()); + } + test.instance[test.methodName].call(test.instance); + } + catch (e:Error) { + // Save a ref to the error to use for reporting + // at the beginning of the next call + ASTest.previousError = e; + } + + // Recurse until done -- note this is not actually a + // tail call because the setTimeout invokes the function + // in the global execution context + setTimeout(function ():void { + ASTest.runNextTest.call(ASTest); + }, ASTest.TEST_INTERVAL); + } + } + + public static function getCompleteListOfTests():void { + var createTestItem:Function = function (item:Object, + methodName:String):Object { + return { + methodName: methodName, + instance: item.instance, + className: item.className, + classDescription: item.classDescription + }; + } + var testList:Array = []; + // No args -- this is being re-invoked from ASTestLoader + // now that we have our tests loaded + for each (var item:Object in ASTest.testClassList) { + var currTestList:Array = []; + var descr:XML; + var hasSetup:Boolean = false; + var hasTeardown:Boolean = false; + descr = flash.utils.describeType( + item.classDescription); + var meth:*; + var methods:Object = {}; + for each (meth in descr..method) { + var methodName:String = meth.@name.toXMLString(); + if (/^test/.test(methodName)) { + methods[methodName] = item; + } + // If there's a setup or teardown somewhere in there + // flag them so we can prepend/append after adding all + // the tests + if (methodName == 'setup') { + hasSetup = true; + } + if (methodName == 'teardown') { + hasTeardown = true; + } + } + + // Normal test methods + // ----- + // If there's an 'order' array defined, run any tests + // it contains in the defined order + var key:String; + if ('order' in item.instance) { + for each (key in item.instance.order) { + // If the item specified in the 'order' list is an actual + // method, add it to the list -- if it doesn't actually exist + // (e.g., if the method has been commented out), just ignore it + if (key in methods) { + currTestList.push(createTestItem(methods[key], key)); + delete methods[key]; + } + } + } + // Run any other methods in whatever order + for (key in methods) { + currTestList.push(createTestItem(methods[key], key)); + } + + // Setup/teardown + // ----- + // Prepend list with setup if one exists + if (hasSetup) { + currTestList.unshift(createTestItem(item, 'setup')); + } + // Append list with teardown if one exists + if (hasTeardown) { + currTestList.push(createTestItem(item, 'teardown')); + } + testList = testList.concat.apply(testList, currTestList); + } + ASTest.testListComplete = testList; + } + } +} + + + diff --git a/org/windmill/astest/ASTestLoader.as b/org/windmill/astest/ASTestLoader.as new file mode 100644 index 0000000..b830f47 --- /dev/null +++ b/org/windmill/astest/ASTestLoader.as @@ -0,0 +1,78 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.astest { + import org.windmill.WMLogger; + import flash.display.Loader; + import flash.display.LoaderInfo; + import flash.net.URLRequest; + import flash.events.Event; + import flash.events.ProgressEvent; + import flash.events.IOErrorEvent; + import flash.system.ApplicationDomain; + import flash.system.SecurityDomain; + import flash.system.LoaderContext; + import flash.external.ExternalInterface; + import flash.utils.getQualifiedClassName; + + public class ASTestLoader { + private static var urls:Array = []; + public static function load(u:Array):void { + urls = u; + loadNext(); + } + private static function loadNext():void { + if (urls.length == 0) { + ASTest.run(); + } + else { + var loader:Loader = new Loader(); + var url:String = urls.shift(); + var req:URLRequest = new URLRequest(url); + // checkPolicyFile is true so it knows to grab the crossdomain.xml + // for wherever it's grabbing tests from + // Need to spoon-feed it the ApplicationDomain and SecurityDomain + // so it knows to load the test SWFs in the the current app context + var ctxt:LoaderContext = new LoaderContext(true, + ApplicationDomain.currentDomain, + SecurityDomain.currentDomain); + // Catch any error that occurs during async load + loader.contentLoaderInfo.addEventListener( + IOErrorEvent.IO_ERROR, function (e:IOErrorEvent):void { + WMLogger.log('Could not load ' + url); + }); + // Handle successful load + loader.contentLoaderInfo.addEventListener( + Event.COMPLETE, function (e:Event):void { + var li:LoaderInfo = e.target as LoaderInfo; + var className:String = getQualifiedClassName(li.loader.content); + var c:Class = ApplicationDomain.currentDomain.getDefinition( + className) as Class; + ASTest.testClassList.push({ + className: className, + classDescription: c, + instance: new c() + }); + loader.unload(); + loadNext(); + }); + loader.load(req, ctxt); + } + } + } +} + + diff --git a/org/windmill/events/Events.as b/org/windmill/events/Events.as new file mode 100644 index 0000000..abd5280 --- /dev/null +++ b/org/windmill/events/Events.as @@ -0,0 +1,168 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.events { + import org.windmill.events.*; + import flash.events.* + import mx.events.* + + public class Events { + public function Events():void {} + + // Allow lengthy list of ordered params or a simple params + // object to override the default values + // NOTE: 'default' param is an array of arrays -- this is + // a janky hack because Object keys in AS3 don't iterate + // in their insertion order + private static function normalizeParams(defaults:Array, + args:Array):Object { + var p:Object = {}; + var elem:*; + // Merge the two arrays into a params obj + for each (elem in defaults) { + p[elem[0]] = elem[1]; + }; + // If there are other params, that means either ordered + // params, or a single params object to set all the options + if (args.length) { + // Ordered params -- use these to override vals + // in the params map + if (args[0] is Boolean) { + // Iterate through the array of param keys to pull + // out any param values passed, in order + for each (elem in defaults) { + p[elem[0]] = args.shift(); + if (!args.length) { + break; + } + } + } + // Options param obj + else { + for (var prop:String in p) { + if (prop in args[0]) { + p[prop] = args[0][prop]; + } + } + } + } + return p; + } + + public static function triggerMouseEvent(obj:*, type:String, + ...args):void { + // AS3 Object keys don't iterate in insertion order + var defaults:Array = [ + ['bubbles', true], // Override the default of false + ['cancelable', false], + ['localX', 0], // Override the default of NaN + ['localY', 0], // Override the default of NaN + ['relatedObject', null], + ['ctrlKey', false], + ['altKey', false], + ['shiftKey', false], + ['buttonDown', false], + ['delta', 0] + ]; + var p:Object = Events.normalizeParams(defaults, args); + var ev:WMMouseEvent = new WMMouseEvent(type, p.bubbles, + p.cancelable, p.localX, p.localY, + p.relatedObject, p.ctrlKey, p.altKey, p.shiftKey, + p.buttonDown, p.delta); + // Check for stageX and stageY in params obj -- these are + // only getters in th superclass, so we don't set them in + // the constructor -- we set them here. + if (args.length && !(args[0] is Boolean)) { + p = args[0]; + if ('stageX' in p) { + ev.stageX = p.stageX; + } + if ('stageY' in p) { + ev.stageY = p.stageY; + } + } + obj.dispatchEvent(ev); + } + + public static function triggerTextEvent(obj:*, type:String, + ...args):void { + // AS3 Object keys don't iterate in insertion order + var defaults:Array = [ + ['bubbles', true], // Override the default of false + ['cancelable', false], + ['text', ''] + ]; + var p:Object = Events.normalizeParams(defaults, args); + var ev:WMTextEvent = new WMTextEvent(type, p.bubbles, + p.cancelable, p.text); + obj.dispatchEvent(ev); + } + + public static function triggerFocusEvent(obj:*, type:String, + ...args):void { + // AS3 Object keys don't iterate in insertion order + var defaults:Array = [ + ['bubbles', true], // Override the default of false + ['cancelable', false], + ['relatedObject', null], + ['shiftKey', false], + ['keyCode', 0] + ]; + var p:Object = Events.normalizeParams(defaults, args); + var ev:WMFocusEvent = new WMFocusEvent(type, p.bubbles, + p.cancelable, p.relatedObject, p.shiftKey, p.keyCode); + obj.dispatchEvent(ev); + } + + public static function triggerKeyboardEvent(obj:*, type:String, + ...args):void { + // AS3 Object keys don't iterate in insertion order + var defaults:Array = [ + ['bubbles', true], // Override the default of false + ['cancelable', false], + ['charCode', 0], + ['keyCode', 0], + ['keyLocation', 0], + ['ctrlKey', false], + ['altKey', false], + ['shiftKey', false] + ]; + var p:Object = Events.normalizeParams(defaults, args); + var ev:WMKeyboardEvent = new WMKeyboardEvent(type, p.bubbles, + p.cancelable, p.charCode, p.keyCode, p.keyLocation, + p.ctrlKey, p.altKey, p.shiftKey); + obj.dispatchEvent(ev); + } + public static function triggerListEvent(obj:*, type:String, + ...args):void { + // AS3 Object keys don't iterate in insertion order + var defaults:Array = [ + ['bubbles', false], // Don't override -- the real one doesn't bubble + ['cancelable', false], + ['columnIndex', -1], + ['rowIndex', -1], + ['reason', null], + ['itemRenderer', null] + ]; + var p:Object = Events.normalizeParams(defaults, args); + var ev:WMListEvent = new WMListEvent(type, p.bubbles, + p.cancelable, p.columnIndex, p.rowIndex, p.reason, + p.itemRenderer); + obj.dispatchEvent(ev); + } + } +} + diff --git a/org/windmill/events/WMFocusEvent.as b/org/windmill/events/WMFocusEvent.as new file mode 100644 index 0000000..9c4d33d --- /dev/null +++ b/org/windmill/events/WMFocusEvent.as @@ -0,0 +1,31 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.events { + import flash.events.FocusEvent; + import flash.display.InteractiveObject; + + public class WMFocusEvent extends FocusEvent { + public function WMFocusEvent(type:String, + bubbles:Boolean = true, cancelable:Boolean = false, + relatedObject:InteractiveObject = null, + shiftKey:Boolean = false, keyCode:uint = 0) { + super(type, bubbles, cancelable, relatedObject, + shiftKey, keyCode); + } + } +} + diff --git a/org/windmill/events/WMKeyboardEvent.as b/org/windmill/events/WMKeyboardEvent.as new file mode 100644 index 0000000..947b661 --- /dev/null +++ b/org/windmill/events/WMKeyboardEvent.as @@ -0,0 +1,33 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.events { + + import flash.events.KeyboardEvent; + + public class WMKeyboardEvent extends KeyboardEvent { + public function WMKeyboardEvent(type:String, + bubbles:Boolean = true, cancelable:Boolean = false, + charCode:uint = 0, keyCode:uint = 0, + keyLocation:uint = 0, ctrlKey:Boolean = false, + altKey:Boolean = false, shiftKey:Boolean = false) { + super(type, bubbles, cancelable, charCode, keyCode, + keyLocation, ctrlKey, altKey, shiftKey); + } + } +} + + diff --git a/org/windmill/events/WMListEvent.as b/org/windmill/events/WMListEvent.as new file mode 100644 index 0000000..3d81ae6 --- /dev/null +++ b/org/windmill/events/WMListEvent.as @@ -0,0 +1,31 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.events { + import mx.events.ListEvent; + import mx.controls.listClasses.IListItemRenderer; + + public class WMListEvent extends ListEvent { + public function WMListEvent(type:String, + bubbles:Boolean = false, cancelable:Boolean = false, + columnIndex:int = -1, rowIndex:int = -1, + reason:String = null, itemRenderer:IListItemRenderer = null) { + super(type, bubbles, cancelable, columnIndex, rowIndex, + reason, itemRenderer); + } + } +} + diff --git a/org/windmill/events/WMMouseEvent.as b/org/windmill/events/WMMouseEvent.as new file mode 100644 index 0000000..504b2ed --- /dev/null +++ b/org/windmill/events/WMMouseEvent.as @@ -0,0 +1,51 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.events { + + import flash.events.MouseEvent; + import flash.display.InteractiveObject; + + public class WMMouseEvent extends MouseEvent { + private var fakeStageX:Number; + private var fakeStageY:Number; + + public function WMMouseEvent(type:String, + bubbles:Boolean = false, cancelable:Boolean = false, + localX:Number = NaN, localY:Number = NaN, + relatedObject:InteractiveObject = null, + ctrlKey:Boolean = false, altKey:Boolean = false, + shiftKey:Boolean = false, buttonDown:Boolean = false, + delta:int = 0) { + super(type, bubbles, cancelable, localX, localY, + relatedObject, ctrlKey, altKey, shiftKey, buttonDown, + delta); + } + public function set stageX(value:Number):void { + fakeStageX = value; + } + override public function get stageX():Number { + return fakeStageX; + } + public function set stageY(value:Number):void { + fakeStageY = value; + } + override public function get stageY():Number { + return fakeStageY; + } + } +} + diff --git a/org/windmill/events/WMTextEvent.as b/org/windmill/events/WMTextEvent.as new file mode 100644 index 0000000..a61107f --- /dev/null +++ b/org/windmill/events/WMTextEvent.as @@ -0,0 +1,29 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package org.windmill.events { + import flash.events.TextEvent; + + public class WMTextEvent extends TextEvent { + public function WMTextEvent(type:String, + bubbles:Boolean = false, cancelable:Boolean = false, + text:String = "") { + super(type, bubbles, cancelable, text); + } + } +} + + diff --git a/pkg/build.json b/pkg/build.json new file mode 100644 index 0000000..e9ed255 --- /dev/null +++ b/pkg/build.json @@ -0,0 +1,22 @@ +{ + "source-path": + ".", + "output": + "windmill.swc", + "include-classes": + [ + "org.windmill.Windmill", + "org.windmill.WMController", + "org.windmill.WMLocator", + "org.windmill.WMLogger", + "org.windmill.WMExplorer", + "org.windmill.WMRecorder", + "org.windmill.WMAssert", + "org.windmill.events.Events", + "org.windmill.events.WMFocusEvent", + "org.windmill.events.WMKeyboardEvent", + "org.windmill.events.WMListEvent", + "org.windmill.events.WMMouseEvent", + "org.windmill.events.WMTextEvent" + ] +} diff --git a/tests/TestApp.mxml b/tests/TestApp.mxml new file mode 100644 index 0000000..b672451 --- /dev/null +++ b/tests/TestApp.mxml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/tests/TestAppCode.as b/tests/TestAppCode.as new file mode 100644 index 0000000..515094d --- /dev/null +++ b/tests/TestAppCode.as @@ -0,0 +1,212 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package { + import mx.core.Application; + import flash.display.MovieClip; + import flash.events.* + import mx.controls.* + import mx.containers.Panel + import mx.events.* + import flash.utils.*; + import flash.net.URLRequest; + import flash.display.Stage; + import flash.display.Sprite; + import flash.geom.Rectangle; + import util.DOMEventDrag; + import flash.display.Sprite; + import flash.geom.Point; + import flash.external.ExternalInterface; + import util.SortedDict; + import org.windmill.WMBootstrap; + import org.windmill.WMLogger; + + public class TestAppCode extends MovieClip { + + public var publicInt:int = 2112; + public var publicString:String = 'Geddy Lee'; + public var publicArray:Array = ['By-Tor', 'Snow Dog']; + + private var stg:Stage; + private var spr:Sprite = new Sprite(); + private var draggable:Sprite; + private var context:*; + private var elems:Object = {}; + private var foo:Sprite; + + public function init(ctxt:Application):void { + context = ctxt; + stg = context.stage; + + var d:SortedDict = new SortedDict(); + d.addItem('itemA', { + foo: 'a', + bar: 'c' + }); + d.addItem('itemB', { + foo: 'b', + bar: 'b' + }); + d.addItem('itemC', { + foo: 'c', + bar: 'a' + }); + var compFoo:Function = function (a:Object, b:Object):int { + return a.foo > b.foo ? 1 : -1; + }; + var compBar:Function = function (a:Object, b:Object):int { + return a.bar > b.bar ? 1 : -1; + }; + d.sort(compFoo); + d.each(function (key:String, val:Object):void { + trace(key + ', foo: ' + val.foo); + }); + d.sort(compBar); + d.each(function (key:String, val:Object):void { + trace(key + ', bar: ' + val.bar); + }); + // Panel + var panel:Panel = new Panel(); + context.addChild(panel); + panel.id = 'mainPanel'; + panel.title = "Windmill Flash Tests"; + + // TextArea + var txtArea:TextArea = new TextArea(); + txtArea.name = 'testTextArea'; + panel.addChild(txtArea); + elems.txtArea = txtArea; + + // Button + var button:Button = new Button(); + button.id = 'howdyButton'; + button.label = 'Howdy'; + panel.addChild(button); + + // Text input + var txtInput:TextInput = new TextInput(); + txtInput.name = 'testTextInput'; + panel.addChild(txtInput); + txtInput.htmlText = 'This is a test.'; + elems.txtInput = txtInput + + var subPanel:Panel = new Panel(); + panel.addChild(subPanel); + subPanel.id = 'subPanel'; + + // Plain text field + var txtField:Text = new Text(); + txtField.name = 'testText'; + subPanel.addChild(txtField); + txtField.htmlText = 'This is some test text. This is a test link'; + + // Combo box (select) + var items:Array = [ + { + dude: 'Geddy', + data: 'bass' + }, + { + dude: 'Neil', + data: 'drums' + }, + { + dude: 'Alex', + data: 'guitar' + } + ]; + var box:ComboBox = new ComboBox(); + box.labelField = 'dude'; + box.name = 'comboTest'; + box.dataProvider = items; + box.selectedItem = items[1]; + subPanel.addChild(box); + + spr.name = 'dragSprite'; + spr.graphics.clear() + spr.graphics.beginFill(0x00ff00); + spr.graphics.drawRect(0,0,100,100); + stg.addChild(spr); + + spr.addEventListener(MouseEvent.MOUSE_DOWN, beginDrag); + stg.addEventListener(MouseEvent.MOUSE_UP, endDrag); + + context.doubleClickEnabled = true; + + WMBootstrap.init(context); + /* + // Focus + stg.addEventListener(FocusEvent.FOCUS_IN, evHandler); + stg.addEventListener(FocusEvent.FOCUS_OUT, evHandler); + // Keyboard + stg.addEventListener(KeyboardEvent.KEY_DOWN, evHandler); + stg.addEventListener(KeyboardEvent.KEY_UP, evHandler); + // Mouse + stg.addEventListener(MouseEvent.MOUSE_DOWN, evHandler); + stg.addEventListener(MouseEvent.MOUSE_UP, evHandler); + //stg.addEventListener(MouseEvent.MOUSE_MOVE, evHandler); + stg.addEventListener(MouseEvent.DOUBLE_CLICK, evHandler); + stg.addEventListener(MouseEvent.CLICK, evHandler); + // Text + stg.addEventListener(TextEvent.TEXT_INPUT, evHandler); + stg.addEventListener(TextEvent.LINK, evHandler); + // ComboBox + box.addEventListener(ListEvent.CHANGE, evHandler); + box.addEventListener(ListEvent.ITEM_ROLL_OVER, evHandler); + box.addEventListener(ListEvent.ITEM_ROLL_OUT, evHandler); + box.addEventListener(DropdownEvent.OPEN, evHandler); + box.addEventListener(DropdownEvent.CLOSE, evHandler); + box.addEventListener(ScrollEvent.SCROLL, evHandler); + */ + + + /* + org.windmill.WMController.click({ + label: 'Howdy' + }); + + org.windmill.WMController.click({ + link: 'This is a test link' + }); + + org.windmill.WMController.type({ + name: 'testText', + text: 'Howdy, sir.' + }); + */ + + } + private function evHandler(e:Event):void { + var targ:* = e.target; + trace(e.toString()); + trace(e.target.toString()); + trace(getQualifiedClassName(e.target)); + } + + private function beginDrag(e:MouseEvent):void { + if (e.target.name == 'dragSprite') { + DOMEventDrag.startDrag(spr); + //spr.startDrag(); + } + } + private function endDrag(e:MouseEvent):void { + if (e.target.name == 'dragSprite') { + DOMEventDrag.stopDrag(spr); + } + //spr.stopDrag(); + } + } +} diff --git a/tests/TestBar.as b/tests/TestBar.as new file mode 100644 index 0000000..427a6e5 --- /dev/null +++ b/tests/TestBar.as @@ -0,0 +1,9 @@ +package { + import org.windmill.TestCase; + + public class TestBar extends TestCase { + public function testUiop():void { + } + } +} + diff --git a/tests/TestFoo.as b/tests/TestFoo.as new file mode 100644 index 0000000..ff8b51a --- /dev/null +++ b/tests/TestFoo.as @@ -0,0 +1,69 @@ +package { + import org.windmill.TestCase; + public class TestFoo extends TestCase { + public var order:Array = ['testClick', 'testClickTimeout', 'testWaitCondition', 'testWaitConditionTimeout', + 'testWaitSleep', 'testAssertDisplayObject', 'testWaitDisplayObject', 'testAssertEqualsString', + 'testAssertEqualsNumber', 'testAppPublicInt', 'testAppPublicString', 'testAppPublicArray']; + + public function setup():void { + } + public function testClick():void { + controller.click({id: 'howdyButton'}); + } + public function testClickTimeout():void { + controller.click({id: 'howdyButton', timeout: 3000}); + } + public function testWaitCondition():void { + var now:Date = new Date(); + var nowTime:Number = now.getTime(); + var thenTime:Number = nowTime + 5000; // Five seconds from now + waits.forCondition({test: function ():Boolean { + var dt:Date = new Date(); + var dtTime:Number = dt.getTime(); + // Wait until the current date is greater + // the thenTime, set above + return (dtTime > thenTime); + }}); + } + public function testWaitConditionTimeout():void { + waits.forCondition({test: function ():Boolean { + return false; + }, timeout: 3000}); + } + public function testWaitSleep():void { + waits.sleep({milliseconds: 5000}); + } + public function testAssertDisplayObject():void { + asserts.assertDisplayObject({id: 'mainPane'}); + } + public function testWaitDisplayObject():void { + waits.forDisplayObject({id: 'mainPanel', timeout: 5000}); + } + public function testAssertEqualsString():void { + var foo:String = 'foo'; + asserts.assertEquals('foo', foo); + } + public function testAssertEqualsNumber():void { + var num:int = 2111; + asserts.assertEquals(2112, num); + } + + // Test some public properties in the main Flex app class + public function testAppPublicInt():void { + var num:int = context.testAppCode.publicInt; + asserts.assertEquals(2112, num); + } + public function testAppPublicString():void { + var str:String = context.testAppCode.publicString; + asserts.assertEquals('Geddy Lee', str); + } + public function testAppPublicArray():void { + var arr:Array = context.testAppCode.publicArray; + asserts.assertEquals('Snow Dog', arr[1]); + } + + public function teardown():void { + } + } +} + diff --git a/tests/WMDemo.as b/tests/WMDemo.as new file mode 100644 index 0000000..afbd54a --- /dev/null +++ b/tests/WMDemo.as @@ -0,0 +1,156 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package { + import flash.display.Loader; + import flash.net.URLRequest; + import flash.system.ApplicationDomain; + import flash.system.SecurityDomain; + import flash.system.LoaderContext; + import flash.display.LoaderInfo; + import flash.events.ProgressEvent; + import flash.events.IOErrorEvent; + import flash.display.MovieClip; + import flash.display.DisplayObject; + import flash.display.Sprite; + import flash.display.StageAlign; + import flash.display.StageScaleMode; + import flash.events.Event; + import flash.events.MouseEvent; + import flash.text.TextField; + import flash.text.StyleSheet; + import flash.utils.*; + import util.DOMEventDrag; + import org.windmill.WMBootstrap; + + [SWF(width="400",height="400",frameRate="30",backgroundColor="#FFFFFF")] + + public class WMDemo extends MovieClip { + private var spriteVert:int = 20; + private var assetList:Object = { + clamwich: { + name: 'Clamwich', + y: 20 + }, + bat_punch: { + name: 'Bat Punch', + y: 100 + }, + cloud_candy: { + name: 'Cloud Candy', + y: 180 + }, + crab_apple: { + name: 'Crab Apple', + y: 260 + } + }; + private var dragged:Sprite; + + public function WMDemo():void { + var createLoadFunc:Function = function (k:String):Function { + return function (res:Event):void { + handleLoadAsset(k, res); + } + } + + stage.scaleMode = StageScaleMode.NO_SCALE; + stage.align = StageAlign.TOP_LEFT; + var item:Object; + for (var key:String in assetList) { + item = assetList[key]; + loadAsset('/images/sp_gift_' + key + '.png', createLoadFunc(key)); + } + stage.addEventListener(MouseEvent.MOUSE_UP, endDrag); + addDragTarget(); + WMBootstrap.init(stage); + } + + // Generic loader function -- takes a handler func for + // loader completion, and an optional handler for loading + // progress + private function loadAsset(url:String, + complete:Function, progress:Function = null):void { + var loader:Loader = new Loader(); + var req:URLRequest = new URLRequest(url); + var con:LoaderContext = new LoaderContext(false, + ApplicationDomain.currentDomain, + SecurityDomain.currentDomain); + // FIXME: Need a nicer error message for when loading breaks + loader.contentLoaderInfo.addEventListener( + IOErrorEvent.IO_ERROR, function ():void { trace('could not load ' + url); }); + loader.contentLoaderInfo.addEventListener( + Event.COMPLETE, complete, false, 0, true); + // If a progress func is defined, pass it the ProgressEvent + if (progress is Function) { + loader.contentLoaderInfo.addEventListener( + ProgressEvent.PROGRESS, progress, false, 0, true); + } + // Doo eeet! + loader.load(req, con); + } + + private function getLoaderContent(e:Event):* { + var li:LoaderInfo = e.target as LoaderInfo; + return li.loader.content; + } + + // Add the background once it loads + private function handleLoadAsset(key:String, e:Event):void { + var result:* = getLoaderContent(e); + var container:Automatable = new Automatable(); + container.addChild(result); + container.x = 20; + var yPos:int = assetList[key].y; + container.y = yPos + container.buttonMode = true; + container.automationName = key + 'Sprite' + container.addEventListener(MouseEvent.MOUSE_DOWN, beginDrag); + addChild(container); + + } + + private function addDragTarget():void { + var spr:Automatable = new Automatable(); + spr.automationName = 'targetSprite'; + spr.graphics.clear() + spr.graphics.beginFill(0xDDDDDD); + spr.graphics.drawRect(0,0,100,100); + spr.x = 300; + spr.y = 150 + addChild(spr); + } + + private function beginDrag(e:MouseEvent):void { + //if (e.target.name == 'dragSprite') { + var targ:Sprite = e.target as Sprite; + dragged = targ; + DOMEventDrag.startDrag(targ); + //} + } + private function endDrag(e:MouseEvent):void { + if (dragged) { + DOMEventDrag.stopDrag(dragged); + } + } + + } +} + +import flash.display.Sprite; +class Automatable extends Sprite { + public var automationName:String; +} diff --git a/tests/build.py b/tests/build.py new file mode 100755 index 0000000..19200b7 --- /dev/null +++ b/tests/build.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +import optparse +import os +import re +import shutil + +# Location of compiler +MXMLC_PATH = 'mxmlc -debug -verbose-stacktraces -incremental=true -compiler.strict -compiler.show-actionscript-warnings' + +# For replacing .as with .swf +as_re = re.compile('\.as$|\.mxml$') + +def app(): + cmd = MXMLC_PATH + ' -source-path=. -source-path+=../../flash ./TestApp.mxml -o ./TestApp.swf' + print cmd + os.system(cmd) + +def tests(): + for root, dirs, file_list in os.walk('./'): + for file in file_list: + if file.endswith('.as') and file != 'TestAppCode.as': + as_file = root + file + swf_file = as_re.sub('.swf', as_file) + # Compile this biyatch + # ----------------------- + cmd = MXMLC_PATH + ' -source-path=. -source-path+=../../flash ' + as_file + ' -o ' + swf_file + #print cmd + os.system(cmd) + +def clean(): + for root, dirs, file_list in os.walk('./'): + for file in file_list: + if file.endswith('.swf') or file.endswith('.swc'): + path = root + '/' + file + cmd = 'rm ' + path + #print cmd + os.system(cmd) + +def parse_opts(): + parser = optparse.OptionParser() + parser.add_option('-t', '--target', dest='target', + help='build TARGET (tests/app/all/clean, default is all)', + metavar='TARGET', choices=('tests', 'app', 'all', 'clean'), default='all') + opts, args = parser.parse_args() + return opts, args + +def main(o, a): + target = o.target + # Build only the AS tests into loadable swfs + if target == 'tests': + tests() + # Build only the test app we use to run the tests against + elif target == 'app': + app() + # Build everything, natch + elif target == 'all': + app() + tests() + # Clean out any swfs in the directory + elif target == 'clean': + clean() + else: + print 'Not a valid target.' + +if __name__ == "__main__": + main(*parse_opts()) + + diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..2a91fe9 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,68 @@ + + + + + + + + + +
+

Testin' Flash

+
+
Here's a text input
+
+
 
+
Here's a select
+
+ +
+
 
+
Here are some radio buttons
+
+  Geddy +  Neil +  Alex +
+
+
+
+
+ + diff --git a/util/DOMEventDrag.as b/util/DOMEventDrag.as new file mode 100644 index 0000000..c92b08f --- /dev/null +++ b/util/DOMEventDrag.as @@ -0,0 +1,120 @@ +/* +Copyright 2009, Matthew Eernisse (mde@fleegix.org) and Slide, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package util { + import flash.display.Sprite; + import flash.events.MouseEvent; + import flash.geom.Point; + import flash.geom.Rectangle; + + /* + * Replacement for AS3 Sprite startDrag/stopDrag + * using only DOM events. + * This allows the drag/drop action to be automated + * using Windmill or other automation frameworks + * that rely on simulated user events + */ + public class DOMEventDrag { + // The Sprite instance to drag + private static var dragSprite:Sprite; + // Pointer offset from top-left corner of the Sprite + private static var offset:Point; + // Lock pointer to the center of the Sprite or not + private static var lockCenter:Boolean; + // Drag constraints, if any, for the drag operation + private static var bounds:Rectangle; + + // For docs on the built-in method, see: + // http://help.adobe.com/en_US/AS3LCR/Flash_10.0/flash/display/Sprite.html#startDrag%28%29 + public static function startDrag(spr:Sprite, + lockCenter:Boolean = false, bounds:Rectangle = null):void { + DOMEventDrag.dragSprite = spr; + DOMEventDrag.lockCenter = lockCenter; + DOMEventDrag.bounds = bounds; + // Event listener for mouse move on the stage + // routed to private doDrag method + spr.stage.addEventListener(MouseEvent.MOUSE_MOVE, DOMEventDrag.doDrag); + } + + // For docs on the built-in method, see: + // http://help.adobe.com/en_US/AS3LCR/Flash_10.0/flash/display/Sprite.html#stopDrag%28%29 + public static function stopDrag(spr:Sprite):void { + // Remove the event listener on the stage + DOMEventDrag.dragSprite.stage.removeEventListener( + MouseEvent.MOUSE_MOVE, + DOMEventDrag.doDrag); + // Remove the dragSprite and the saved offset + DOMEventDrag.dragSprite = null; + DOMEventDrag.offset = null; + } + + private static function doDrag(e:MouseEvent):void { + var dr:Sprite = DOMEventDrag.dragSprite; + // If there's a dragSprite, drag that mofo + // The dragSprite is removed in stopDrag + if (dr) { + var dragX:int; + var dragY:int; + var coordsAbs:Point = new Point(e.stageX, e.stageY); + // Get the local coors of the sprite's container + var coordsLocal:Point = dr.parent.globalToLocal(coordsAbs); + // Since we don't get access to the inital mouse click, + // calculate the offset on the very first mouse move. + // Once the offset is set, blow by this every time. + // stopDrag clears the saved offset until the next drag + if (!DOMEventDrag.offset) { + // Lock the cursor to the center of the Sprite + // Set in the constructor -- defaults to false + if (DOMEventDrag.lockCenter) { + var offX:int = dr.width / 2; + var offY:int = dr.height / 2; + DOMEventDrag.offset = new Point(offX, offY); + } + // Otherwise remember where the click happened, + // and preserve the offset + else { + DOMEventDrag.offset = new Point(coordsLocal.x - dr.x, + coordsLocal.y - dr.y); + } + } + // Adjust the x/y by the offset -- either pointer position + // or to center of the Sprite + dragX = coordsLocal.x - DOMEventDrag.offset.x; + dragY = coordsLocal.y - DOMEventDrag.offset.y; + // Observe any drag constraints + var bounds:Rectangle = DOMEventDrag.bounds; + if (bounds) { + // Left bound + dragX = dragX < bounds.left ? + bounds.left : dragX; + // Top bound + dragY = dragY < bounds.top ? + bounds.top : dragY; + // Right bound + dragX = (dragX + dr.width) > bounds.right ? + (bounds.right - dr.width) : dragX; + // Left bound + dragY = (dragY + dr.height) > bounds.bottom ? + (bounds.bottom - dr.height) : dragY; + } + dr.x = Math.round(dragX); + dr.y = Math.round(dragY); + } + } + } +} + +