You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flex.apache.org by ah...@apache.org on 2014/03/21 19:55:56 UTC

[10/10] git commit: [flex-asjs] [refs/heads/develop] - mustella equivalents in java for marmotinni

mustella equivalents in java for marmotinni


Project: http://git-wip-us.apache.org/repos/asf/flex-asjs/repo
Commit: http://git-wip-us.apache.org/repos/asf/flex-asjs/commit/8a08cdd2
Tree: http://git-wip-us.apache.org/repos/asf/flex-asjs/tree/8a08cdd2
Diff: http://git-wip-us.apache.org/repos/asf/flex-asjs/diff/8a08cdd2

Branch: refs/heads/develop
Commit: 8a08cdd23ec008a13a38ae5fee6ca4d3bccf300b
Parents: a65ada5
Author: Alex Harui <ah...@apache.org>
Authored: Fri Mar 21 08:22:46 2014 -0700
Committer: Alex Harui <ah...@apache.org>
Committed: Fri Mar 21 08:22:46 2014 -0700

----------------------------------------------------------------------
 .../src/marmotinni/AssertPropertyValue.java     | 104 +++++++
 mustella/java/src/marmotinni/AssertStep.java    |  54 ++++
 .../java/src/marmotinni/DispatchKeyEvent.java   | 203 +++++++++++++
 .../src/marmotinni/DispatchMouseClickEvent.java | 205 +++++++++++++
 .../java/src/marmotinni/DispatchMouseEvent.java | 222 ++++++++++++++
 .../java/src/marmotinni/MarmotinniRunner.java   | 250 ++++++++++++++++
 mustella/java/src/marmotinni/SetProperty.java   | 119 ++++++++
 mustella/java/src/marmotinni/TestCase.java      | 293 +++++++++++++++++++
 mustella/java/src/marmotinni/TestOutput.java    |  36 +++
 mustella/java/src/marmotinni/TestStep.java      | 124 ++++++++
 10 files changed, 1610 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/AssertPropertyValue.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/AssertPropertyValue.java b/mustella/java/src/marmotinni/AssertPropertyValue.java
new file mode 100644
index 0000000..253cc49
--- /dev/null
+++ b/mustella/java/src/marmotinni/AssertPropertyValue.java
@@ -0,0 +1,104 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import org.openqa.selenium.JavascriptExecutor;
+
+import org.xml.sax.Attributes;
+
+/** 
+ * AssertPropertyValue
+ *
+ * Assert if a property is set to the right value
+ */
+public class AssertPropertyValue extends AssertStep { 
+
+
+	public AssertPropertyValue () { 
+	}
+	
+	/**
+	 *  The name of the property to test
+	 */
+	public String propertyName;
+	
+	/**
+	 *  The value the property should have
+	 */
+	public String value;
+
+	/**
+	 *  The value the property should have
+	 */
+	public String valueExpression;
+	
+	@Override
+	public void populateFromAttributes(Attributes attributes)
+	{
+		target = attributes.getValue("target");
+		propertyName = attributes.getValue("propertyName");
+		value = attributes.getValue("value");
+		valueExpression = attributes.getValue("valueExpression");
+	}
+	
+	@Override
+    protected void doStep()
+    {
+		
+		StringBuilder getScript = new StringBuilder();
+		insertTargetScript(getScript, target);
+		getScript.append("if (typeof(target['get_' + '" + propertyName + "']) == 'function') return target['get_' + '" + propertyName + "']();");
+		getScript.append(" else return target['" + propertyName + "'];");
+		if (TestStep.showScripts)
+			System.out.println(getScript.toString());
+		String actualValue = ((JavascriptExecutor)webDriver).executeScript(getScript.toString()).toString();
+		String valueString = null;
+		if (valueExpression != null)
+			valueString = ((JavascriptExecutor)webDriver).executeScript(valueExpression).toString();
+		else if (value != null)
+			valueString = value;
+		else
+			valueString = "null";
+			
+		if (!valueString.equals(actualValue))
+		{
+			testResult.doFail(target + "." + propertyName + " " + actualValue + " != " + valueString);	
+		}
+				
+    }
+	
+    /**
+     *  customize string representation
+     */
+	@Override
+    public String toString()
+    {
+		String s = "AssertPropertyValue";
+		if (target != null)
+			s += ": target = " + target.toString();
+		if (propertyName != null)
+			s += ": propertyName = " + propertyName.toString();
+		if (value != null)
+			s += ": value = " + value;
+		return s;
+	}
+	
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/AssertStep.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/AssertStep.java b/mustella/java/src/marmotinni/AssertStep.java
new file mode 100644
index 0000000..a8223f7
--- /dev/null
+++ b/mustella/java/src/marmotinni/AssertStep.java
@@ -0,0 +1,54 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import org.openqa.selenium.WebDriver;
+
+/** 
+ * AssertStep
+ *
+ * Base class for test steps like AssertPropertyValue, etc.
+ */
+public class AssertStep extends TestStep { 
+
+
+	public AssertStep () { 
+	}
+
+	/**
+	 *  Called by the test case in case you need to set up before execute()
+	 */
+	public void preview(WebDriver webDriver, TestCase testCase, TestResult testResult)
+	{
+		this.webDriver = webDriver;
+		this.testCase = testCase;
+		this.testResult = testResult;
+	}
+	
+	public String target;
+	
+	/**
+	 *  Called by the test case in case you need to clean up after execute()
+	 */
+	public void cleanup()
+	{
+	}
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/DispatchKeyEvent.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/DispatchKeyEvent.java b/mustella/java/src/marmotinni/DispatchKeyEvent.java
new file mode 100644
index 0000000..1361432
--- /dev/null
+++ b/mustella/java/src/marmotinni/DispatchKeyEvent.java
@@ -0,0 +1,203 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import java.util.ArrayList;
+
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import org.xml.sax.Attributes;
+
+/** 
+ * DispatchKeyEvent
+ *
+ * Dispatch keys to the currently focused element
+ */
+public class DispatchKeyEvent extends TestStep { 
+
+	public DispatchKeyEvent () { 
+	}
+
+	// These are constants from flash.ui.Keyboard.  They are not
+	// available in non-AIR compilations, so they are reproduced here
+	// to avoid compile errors.
+	// If they change in AIR, these will need to be updated.
+	public static final int FLASH_UI_KEYBOARD_BACK = 0x01000016;
+	public static final int FLASH_UI_KEYBOARD_MENU = 0x01000012;
+	public static final int FLASH_UI_KEYBOARD_SEARCH = 0x0100001F;
+	
+    private StringBuilder charSequence;
+    private int currentRepeat;
+    private int currentKey;
+    private boolean sendBoth;
+    
+    /**
+     *  Set the target's property to the specified value
+     */
+	@Override
+    protected void doStep()
+    {
+        sendBoth = false;
+		
+        if (type == null)
+        {
+            sendBoth = true;
+            type = "keyDown";
+        }
+        
+        int i;
+        int j;
+        int n;
+        charSequence = new StringBuilder();
+				
+        try
+        {
+			for (i = 0; i < repeatCount; i++)
+			{
+				WebElement focusedElement = (WebElement)((JavascriptExecutor)webDriver).executeScript("return document.activeElement");
+				if (chars != null)
+				{
+					charSequence.append(chars);
+					focusedElement.sendKeys(charSequence);
+				}
+				else if (keys != null)
+				{
+					n = keys.size();
+					for (j = 0; j < n; i++)
+					{
+						Keys key = Keys.valueOf(keys.get(j));
+						focusedElement.sendKeys(key);
+						// could be new focused element if tab or similar
+						focusedElement = (WebElement)((JavascriptExecutor)webDriver).executeScript("return document.activeElement");
+					}
+				}
+				else
+				{
+					testResult.doFail("no keys specified");
+					return;
+				}
+			}
+		}
+		catch (Exception e1)
+		{
+			TestOutput.logResult("Exception thrown in DispatchKeyEvent.");
+			testResult.doFail (e1.getMessage()); 
+		}
+		
+    }
+	
+    /**
+     *  (Optional) name of a UI object whose Window/Stage
+     *  will be used to dispatch the event
+     */
+    public String window;
+	
+    /**
+     *  The type of the event to send (keyUp, keyDown, etc).
+     *  If not set, we'll send both a keyDown and a keyUp
+     */
+    public String type;
+	
+    /**
+     *  The char or sequence of chars to send as a string/char if you don't know the charCode (optional)
+     */
+    public String chars;
+	
+    /**
+     *  The ctrlKey property on the KeyboardEvent (optional)
+     */
+    public boolean ctrlKey;
+	
+    /**
+     *  The sequence of keys (optional) e.g ["LEFT", "UP"]
+     */
+    public ArrayList<String> keys;
+	
+    /**
+     *  The keyLocation property on the KeyboardEvent (optional)
+     */
+    public int keyLocation;
+	
+    /**
+     *  The number of times to repeat the sequence (optional)
+     */
+    public int repeatCount = 1;
+	
+    /**
+     *  The shiftKey property on the KeyboardEvent (optional)
+     */
+    public boolean shiftKey;
+	
+    /**
+     *  Designate the created event to be cancelable. by default, they are not
+     */
+    public boolean cancelable = false;
+	    
+    /**
+     *  customize string representation
+     */
+	@Override
+    public String toString()
+    {
+		String s = "DispatchKeyEvent";
+		if (chars != null)
+			s += ": char = " + chars;
+		if (keys != null)
+			s += ": keys = " + keys.toString();
+		if (type != null)
+			s += ", type = " + type;
+		if (shiftKey)
+			s += ", shiftKey = true";
+		if (ctrlKey)
+			s += ", ctrlKey = true";
+		if (repeatCount != 0)
+			s += ", repeatCount = " + Integer.toString(repeatCount);
+		return s;
+	}
+
+	@Override
+	public void populateFromAttributes(Attributes attributes) throws Exception
+	{
+		chars = attributes.getValue("char");
+		String key = attributes.getValue("key");
+		if (key != null)
+		{
+			keys = new ArrayList<String>();
+			keys.add(key);
+		}
+		String keyAttr = attributes.getValue("keys");
+		if (keyAttr != null)
+		{
+			keys = new ArrayList<String>();
+			String keyList[] = keyAttr.split(",");
+			for (String keyPart : keyList)
+				keys.add(keyPart);
+		}
+		String keyCode = attributes.getValue("keyCode");
+		if (keyCode != null)
+			throw new Exception("keyCode not supported");
+
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/DispatchMouseClickEvent.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/DispatchMouseClickEvent.java b/mustella/java/src/marmotinni/DispatchMouseClickEvent.java
new file mode 100644
index 0000000..66c65bc
--- /dev/null
+++ b/mustella/java/src/marmotinni/DispatchMouseClickEvent.java
@@ -0,0 +1,205 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import org.xml.sax.Attributes;
+
+/** 
+ * DispatchMouseClickEvent
+ *
+ * Dispatch click to the element under the mouse
+ */
+public class DispatchMouseClickEvent extends TestStep { 
+
+    /**
+     *  Dispatch click event
+     */
+	@Override
+    protected void doStep()
+    {
+		
+		Long x;
+		Long y;
+		if (hasLocal)
+		{
+			StringBuilder script = new StringBuilder();
+			insertTargetScript(script, target);
+			script.append("return target.element.offsetLeft");
+			if (TestStep.showScripts)
+				System.out.println(script);
+			x = (Long)((JavascriptExecutor)webDriver).executeScript(script.toString());
+			script = new StringBuilder();
+			insertTargetScript(script, target);
+			script.append("return target.element.offsetTop");
+			if (TestStep.showScripts)
+				System.out.println(script);
+			y = (Long)((JavascriptExecutor)webDriver).executeScript(script.toString());
+			x += localX;
+			y += localY;
+		}
+		else
+		{
+			x = stageX;
+			y = stageY;
+		}
+		
+		// find top-most element
+		StringBuilder script = new StringBuilder();
+		script.append("var all = document.all;");
+		script.append("var n = all.length;");
+		script.append("for(var i=n-1;i>=0;i--) { ");
+		script.append("    var e = all[i];");
+		script.append("     if (" + x + " >= e.offsetLeft && " + x + " <= e.offsetLeft + e.offsetWidth && " + y + " >= e.offsetTop && " + y + " <= e.offsetTop + e.offsetHeight)");
+		script.append("         return e;");
+		script.append("};");
+		script.append("return null;");
+		if (TestStep.showScripts)
+			System.out.println(script);
+		WebElement mouseTarget = (WebElement)((JavascriptExecutor)webDriver).executeScript(script.toString());
+        try
+        {
+			mouseTarget.click();
+        }
+        catch (Exception e1)
+        {
+            TestOutput.logResult("Exception thrown in DispatchMouseClickEvent.");
+            testResult.doFail (e1.getMessage()); 
+        }
+		
+    }
+	
+	/**
+	 *  The object that should receive the mouse event
+	 */
+	public String target;
+	
+	/**
+	 *  The ctrlKey property on the MouseEvent (optional)
+	 */
+	public boolean ctrlKey;
+	
+	/**
+	 *  The delta property on the MouseEvent (optional)
+	 */
+	public int delta;
+	
+	private boolean hasLocal;
+	private boolean hasStage;
+	
+	/**
+	 *  The localX property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long localX;
+	
+	/**
+	 *  The localY property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long localY;
+	
+	/**
+	 *  The stageX property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long stageX;
+	
+	/**
+	 *  The stageY property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long stageY;
+	
+	/**
+	 *  The shiftKey property on the MouseEvent (optional)
+	 */
+	public boolean shiftKey;
+	
+	/**
+	 *  The relatedObject property on the MouseEvent (optional)
+	 */
+	public String relatedObject;
+	
+	/**
+	 *  customize string representation
+	 */
+	@Override 
+	public String toString()
+	{
+		String s = "DispatchMouseClickEvent: target = ";
+		s += target;
+		if (hasLocal)
+		{
+			s += ", localX = " + Long.toString(localX);
+			s += ", localY = " + Long.toString(localY);
+		}
+		if (hasStage)
+		{
+			s += ", stageX = " + Long.toString(stageX);
+			s += ", stageY = " + Long.toString(stageY);
+		}
+		if (shiftKey)
+			s += ", shiftKey = true";
+		if (ctrlKey)
+			s += ", ctrlKey = true";
+		
+		if (relatedObject != null)
+			s += ", relatedObject = " + relatedObject;
+		if (delta != 0)
+			s += ", delta = " + Integer.toString(delta);
+		return s;
+	}
+	
+	@Override
+	public void populateFromAttributes(Attributes attributes)
+	{
+		target = attributes.getValue("target");
+		String value = attributes.getValue("localX");
+		if (value != null)
+		{
+			localX = Long.parseLong(value);
+			hasLocal = true;
+		}
+		value = attributes.getValue("localY");
+		if (value != null)
+		{
+			localY = Long.parseLong(value);
+			hasLocal = true;
+		}
+		value = attributes.getValue("stageX");
+		if (value != null)
+		{
+			stageX = Long.parseLong(value);
+			hasStage = true;
+		}
+		value = attributes.getValue("stageY");
+		if (value != null)
+		{
+			stageY = Long.parseLong(value);
+			hasStage = true;
+		}
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/DispatchMouseEvent.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/DispatchMouseEvent.java b/mustella/java/src/marmotinni/DispatchMouseEvent.java
new file mode 100644
index 0000000..0f9a874
--- /dev/null
+++ b/mustella/java/src/marmotinni/DispatchMouseEvent.java
@@ -0,0 +1,222 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.remote.RemoteWebElement;
+
+import org.xml.sax.Attributes;
+
+/** 
+ * DispatchMouseEvent
+ *
+ * Dispatch mouse event
+ */
+public class DispatchMouseEvent extends TestStep { 
+
+    /**
+     *  Dispatch a mouse event
+     */
+	@Override
+    protected void doStep()
+    {
+		
+		Long x;
+		Long y;
+		if (hasLocal)
+		{
+			StringBuilder script = new StringBuilder();
+			insertTargetScript(script, target);
+			script.append("return target.element.offsetLeft");
+			if (TestStep.showScripts)
+				System.out.println(script);
+			x = (Long)((JavascriptExecutor)webDriver).executeScript(script.toString());
+			script = new StringBuilder();
+			insertTargetScript(script, target);
+			script.append("return target.element.offsetTop");
+			if (TestStep.showScripts)
+				System.out.println(script);
+			y = (Long)((JavascriptExecutor)webDriver).executeScript(script.toString());
+			x += localX;
+			y += localY;
+		}
+		else
+		{
+			x = stageX;
+			y = stageY;
+		}
+		
+		// find top-most element
+		StringBuilder script = new StringBuilder();
+		script.append("var all = document.all;");
+		script.append("var n = all.length;");
+		script.append("for(var i=n-1;i>=0;i--) { ");
+		script.append("    var e = all[i];");
+		script.append("     if (" + x + " >= e.offsetLeft && " + x + " <= e.offsetLeft + e.offsetWidth && " + y + " >= e.offsetTop && " + y + " <= e.offsetTop + e.offsetHeight) {");
+		script.append("         if (!e.id) e.id = Math.random().toString();");
+		script.append("         return e;");
+		script.append("     }");
+		script.append("};");
+		script.append("return null;");
+		if (TestStep.showScripts)
+			System.out.println(script);
+		RemoteWebElement mouseTarget = (RemoteWebElement)((JavascriptExecutor)webDriver).executeScript(script.toString());
+		//System.out.println("mouseTarget: " + mouseTarget.getTagName() + " " + mouseTarget.getAttribute("id"));
+		String actualId = mouseTarget.getAttribute("id");
+		
+		script = new StringBuilder();
+		script.append("var e = document.createEvent('MouseEvent');");
+		script.append("e.initMouseEvent(\"" + type + "\", true, false, window, " + delta + ", " + x + ", " + y + ", " + localX + ", " + localY + ", " + ctrlKey + ", false, " + shiftKey + ", false, " + type.equals("mouseDown") + ", " + relatedObject + ");");
+		script.append("document.getElementById('" + actualId + "').dispatchEvent(e);");
+		if (TestStep.showScripts)
+			System.out.println(script);
+        try
+        {
+			((JavascriptExecutor)webDriver).executeScript(script.toString());
+        }
+        catch (Exception e1)
+        {
+            TestOutput.logResult("Exception thrown in DispatchMouseEvent.");
+            testResult.doFail (e1.getMessage()); 
+        }
+		
+    }
+	
+	/**
+	 *  The object that should receive the mouse event
+	 */
+	public String target;
+	
+	/**
+	 *  The type/name of the event
+	 */
+	public String type;
+	
+	/**
+	 *  The ctrlKey property on the MouseEvent (optional)
+	 */
+	public boolean ctrlKey;
+	
+	/**
+	 *  The delta property on the MouseEvent (optional)
+	 */
+	public int delta;
+	
+	private boolean hasLocal;
+	private boolean hasStage;
+
+	/**
+	 *  The localX property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long localX;
+	
+	/**
+	 *  The localY property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long localY;
+	
+	/**
+	 *  The stageX property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long stageX;
+	
+	/**
+	 *  The stageY property on the MouseEvent (optional)
+	 *  Either set stageX/stageY or localX/localY, but not both.
+	 */
+	public long stageY;
+	
+	/**
+	 *  The shiftKey property on the MouseEvent (optional)
+	 */
+	public boolean shiftKey;
+	
+	/**
+	 *  The relatedObject property on the MouseEvent (optional)
+	 */
+	public String relatedObject;
+	
+	/**
+	 *  customize string representation
+	 */
+	@Override 
+	public String toString()
+	{
+		String s = "DispatchMouseEvent: target = ";
+		s += target;
+		if (hasLocal)
+		{
+			s += ", localX = " + Long.toString(localX);
+			s += ", localY = " + Long.toString(localY);
+		}
+		if (hasStage)
+		{
+			s += ", stageX = " + Long.toString(stageX);
+			s += ", stageY = " + Long.toString(stageY);
+		}
+		if (shiftKey)
+			s += ", shiftKey = true";
+		if (ctrlKey)
+			s += ", ctrlKey = true";
+		
+		if (relatedObject != null)
+			s += ", relatedObject = " + relatedObject;
+		if (delta != 0)
+			s += ", delta = " + Integer.toString(delta);
+		return s;
+	}
+	
+	@Override
+	public void populateFromAttributes(Attributes attributes)
+	{
+		target = attributes.getValue("target");
+		type = attributes.getValue("type");
+		String value = attributes.getValue("localX");
+		if (value != null)
+		{
+			localX = Long.parseLong(value);
+			hasLocal = true;
+		}
+		value = attributes.getValue("localY");
+		if (value != null)
+		{
+			localY = Long.parseLong(value);
+			hasLocal = true;
+		}
+		value = attributes.getValue("stageX");
+		if (value != null)
+		{
+			stageX = Long.parseLong(value);
+			hasStage = true;
+		}
+		value = attributes.getValue("stageY");
+		if (value != null)
+		{
+			stageY = Long.parseLong(value);
+			hasStage = true;
+		}
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/MarmotinniRunner.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/MarmotinniRunner.java b/mustella/java/src/marmotinni/MarmotinniRunner.java
new file mode 100644
index 0000000..a2f85e4
--- /dev/null
+++ b/mustella/java/src/marmotinni/MarmotinniRunner.java
@@ -0,0 +1,250 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.firefox.FirefoxDriver;
+import org.openqa.selenium.chrome.ChromeDriver;
+
+import java.io.IOException;
+import java.nio.CharBuffer;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+
+/** 
+ * TestEngine
+ *
+ * Runs a mustella script by parsing it and executing its contents
+ *
+ */
+public class MarmotinniRunner extends DefaultHandler { 
+
+	public MarmotinniRunner () { 
+	}
+
+	public boolean runTest(WebDriver webDriver, String scriptName)
+	{
+		if (tagMap == null)
+			setupMap();
+		tests = new ArrayList<TestCase>();
+		
+		System.out.println("running script " + scriptName);
+		
+		SAXParserFactory spf = SAXParserFactory.newInstance();
+		try {
+			
+			//get a new instance of parser
+			SAXParser sp = spf.newSAXParser();
+			
+			//parse the file and also register this class for call backs
+			sp.parse(scriptName, this);
+			
+		}catch(SAXException se) {
+			se.printStackTrace();
+		}catch(ParserConfigurationException pce) {
+			pce.printStackTrace();
+		}catch (IOException ie) {
+			ie.printStackTrace();
+		}
+		boolean success = true;
+		System.out.println("test case count: " + tests.size());
+		for (TestCase testCase : tests)
+		{
+			this.testCase = testCase;
+			testCase.runTest(webDriver);
+			TestResult testResult = testCase.getTestResult();
+			testResult.scriptName = scriptName;
+			if (!testResult.hasStatus())
+				testResult.result = TestResult.PASS;
+			else
+			{
+				System.out.println("test failure");
+				success = false;
+			}
+			System.out.println(testResult.toString());
+		}
+		return success;
+	}
+
+	private HashMap<String, Class<?>> tagMap = null;
+	
+	private void setupMap()
+	{
+		tagMap = new HashMap<String, Class<?>>();
+		tagMap.put("SetProperty", SetProperty.class);
+		tagMap.put("DispatchKeyEvent", DispatchKeyEvent.class);
+		tagMap.put("DispatchMouseEvent", DispatchMouseEvent.class);
+		tagMap.put("DispatchMouseClickEvent", DispatchMouseClickEvent.class);
+		tagMap.put("AssertPropertyValue", AssertPropertyValue.class);
+	}
+	
+	private boolean verboseXMLParsing;
+	private ArrayList<TestCase> tests;
+	private TestCase testCase;
+	private String currentPhase;
+	
+	//Event Handlers
+	public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+		if (verboseXMLParsing)
+			System.out.println("StartElement: uri: " + uri + "localName: " + localName + "qName: " + qName);
+		if(qName.equalsIgnoreCase("UnitTester")) {
+			// this should be the top-level tag, just ignore for now.  Will need to process script subtags later
+		}
+		else if (qName.length() == 0)
+		{
+			// there seems to be some empty tags
+		}
+		else if (qName.equalsIgnoreCase("testCases"))
+		{
+			// there seems to be some empty tags
+		}
+		else if (qName.equalsIgnoreCase("mx:Script"))
+		{
+			// there seems to be some empty tags
+		}
+		else if (qName.equalsIgnoreCase("mx:Metadata"))
+		{
+			// there seems to be some empty tags
+		}
+		else if (qName.equalsIgnoreCase("TestCase")) {
+			testCase = new TestCase();
+			testCase.populateFromAttributes(attributes);
+			tests.add(testCase);
+		}
+		else if (tagMap.containsKey(qName)) {
+			Class<?> c = tagMap.get(qName);
+			try {
+				TestStep testStep = (TestStep)c.newInstance(); 
+				testStep.populateFromAttributes(attributes);
+				if (currentPhase.equals("setup"))
+					testCase.setup.add(testStep);
+				else if (currentPhase.equals("body"))
+					testCase.body.add(testStep);
+				else
+					testCase.cleanup.add(testStep);
+			}
+			catch (Exception e) {
+				System.out.println(e.getMessage());
+				return;
+			}
+		}
+		else if (qName.equals("setup")) {
+			currentPhase = "setup";
+			testCase.setup = new ArrayList<TestStep>();
+		}
+		else if (qName.equals("body")) {
+			currentPhase = "body";
+			testCase.body = new ArrayList<TestStep>();
+		}
+		else if (qName.equals("cleanup")) {
+			currentPhase = "cleanup";
+			testCase.cleanup = new ArrayList<TestStep>();
+		}
+		else {
+			System.out.println("unexpected element: " + uri + qName);
+		}
+
+	}
+	
+	
+	public void characters(char[] ch, int start, int length) throws SAXException {
+		CharBuffer cb = CharBuffer.allocate(ch.length);
+		cb.put(ch);
+		String s = cb.toString();
+		if (verboseXMLParsing)
+			if (s.trim().length() > 0)
+				System.out.println("unexpected characters: " + s);
+	}
+	
+	public void endElement(String uri, String localName, String qName) throws SAXException {
+		if (verboseXMLParsing)
+			System.out.println("EndElement: uri: " + uri + "localName: " + localName + "qName: " + qName);
+	}
+	
+	public static void main(String[] args)
+    {
+		ArrayList<String> scripts = new ArrayList<String>();
+        Map<String, String> argsMap = new HashMap<String, String>();
+        for (String arg : args)
+        {
+            String[] keyValuePair = arg.split("=");
+			if (keyValuePair[0].equals("script"))
+				scripts.add(keyValuePair[1]);
+			else	
+				argsMap.put(keyValuePair[0], keyValuePair[1]);
+        }
+        final String showScriptsArg = argsMap.get("showScripts");
+        TestStep.showScripts = showScriptsArg != null && showScriptsArg.equalsIgnoreCase("true");
+        final String showStepsArg = argsMap.get("showSteps");
+        TestCase.showSteps = showStepsArg != null && showStepsArg.equalsIgnoreCase("true");
+		
+        final String url = argsMap.get("url");
+		System.out.println(url);
+        
+		final String browser = argsMap.get("browser");
+        WebDriver driver;
+		if (browser != null && browser.equalsIgnoreCase("chrome"))
+			driver = new ChromeDriver();
+		else
+			driver = new FirefoxDriver();
+		
+        driver.get(url);
+		
+		int exitCode = 0;
+		try 
+		{
+			MarmotinniRunner mr = new MarmotinniRunner();
+			final String verboseXMLParsingArg = argsMap.get("verboseXMLParsing");
+			mr.verboseXMLParsing = verboseXMLParsingArg != null && verboseXMLParsingArg.equalsIgnoreCase("true");
+			int n = scripts.size();
+			for (int i = 0; i < n; i++)
+			{
+				if (!mr.runTest(driver, scripts.get(i)))
+				{
+					System.out.println("script failed");
+					exitCode = 1;
+				}
+			}			
+		}
+		catch (Exception e)
+        {
+            System.out.println(e.getMessage());
+			exitCode = 1;
+        }
+        finally
+        {
+            driver.quit();
+        }
+		System.exit(exitCode);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/SetProperty.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/SetProperty.java b/mustella/java/src/marmotinni/SetProperty.java
new file mode 100644
index 0000000..b05c419
--- /dev/null
+++ b/mustella/java/src/marmotinni/SetProperty.java
@@ -0,0 +1,119 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import org.xml.sax.Attributes;
+
+/** 
+ * SetProperty
+ *
+ * Set a property
+ */
+public class SetProperty extends TestStep { 
+
+	public SetProperty () { 
+	}
+
+    /**
+     *  Set the target's property to the specified value
+     */
+	@Override
+    protected void doStep()
+    {
+		String valueString = null;
+		if (valueExpression != null)
+			valueString = ((JavascriptExecutor)webDriver).executeScript(valueExpression).toString();
+		else if (value != null)
+		{
+			if (value.equalsIgnoreCase("false") || value.equalsIgnoreCase("true"))
+				valueString = value;
+			else {				
+				try {
+					Double.parseDouble(value);
+					valueString = value;
+				}
+				catch (Exception e) {
+					valueString = "'" + value + "'";
+				}
+			}
+		}
+		else
+			valueString = "null";
+		
+		StringBuilder setScript = new StringBuilder();
+		insertTargetScript(setScript, target);
+		setScript.append("if (typeof(target['set_' + '" + propertyName + "']) == 'function') target['set_' + '" + propertyName + "'](" + valueString + ");");
+		setScript.append(" else target['" + propertyName + "']=" + valueString + ";");
+		if (TestStep.showScripts)
+			System.out.println(setScript.toString());
+		((JavascriptExecutor)webDriver).executeScript(setScript.toString());
+    }
+	
+	/**
+	 *  The object to set a property on
+	 */
+	public String target;
+	
+	/**
+	 *  The name of the property to set
+	 */
+	public String propertyName;
+	
+	/**
+	 *  The value to set
+	 */
+	public String value;
+	
+	/**
+	 *  The value to set
+	 */
+	public String valueExpression;
+	
+    /**
+     *  customize string representation
+     */
+	@Override
+    public String toString()
+    {
+		String s = "SetProperty";
+		if (target != null)
+			s += ": target = " + target.toString();
+		if (propertyName != null)
+			s += ": propertyName = " + propertyName.toString();
+		if (value != null)
+			s += ": value = " + value;
+		return s;
+	}
+
+	@Override
+	public void populateFromAttributes(Attributes attributes)
+	{
+		target = attributes.getValue("target");
+		propertyName = attributes.getValue("propertyName");
+		value = attributes.getValue("value");
+		valueExpression = attributes.getValue("valueExpression");
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/TestCase.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/TestCase.java b/mustella/java/src/marmotinni/TestCase.java
new file mode 100644
index 0000000..06e0e80
--- /dev/null
+++ b/mustella/java/src/marmotinni/TestCase.java
@@ -0,0 +1,293 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Date;
+
+import org.openqa.selenium.WebDriver;
+
+import org.xml.sax.Attributes;
+
+/** 
+ * TestCase
+ *
+ * A set of TestSteps.
+ */
+public class TestCase { 
+
+	public static boolean showSteps = false;
+	
+	/**
+	 *  The history of bugs that this test case has encountered
+	 */
+	public List<String> bugs;
+	
+	/**
+	 *  The sequence of TestSteps that comprise the test setup
+	 */
+	public List<TestStep> setup;
+	
+	/**
+	 *  The sequence of TestSteps that comprise the test
+	 */
+	public List<TestStep> body;
+	
+	/**
+	 *  The sequence of TestSteps that restore the test environment
+	 */
+	public List<TestStep> cleanup;
+	
+	/**
+	 *  An identifier for the test
+	 */
+	public String testID;
+	
+	/**
+	 *  A description of the test 
+	 */
+	public String description;
+	
+	/**
+	 *  keywords, summarizing the tests
+	 */
+	public String keywords;
+	
+	/**
+	 *  frequency, an estimate of the tests's intersection with real-world
+	 *  usage. 1 = most frequent usage; 2 somewhat common; 3 = unusual
+	 */
+	public String frequency;
+	
+	/**
+	 *  The current set of steps (setup, body, cleanup) we are executing
+	 */
+	private List<TestStep> currentSteps;
+	
+	/**
+	 *  Which step we're currently executing (or waiting on an event for)
+	 */
+	private int currentIndex = 0;
+	
+	/**
+	 *  Number of steps in currentSteps
+	 */
+	private int numSteps = 0;
+	
+	/**
+	 *  Storage for the cleanupAsserts
+	 */
+	private List<AssertStep> cleanupAsserts;
+	
+	/**
+	 *  Steps we have to review at the end of the body to see if
+	 *  they failed or not.  These steps monitor activity like
+	 *  checking for duplicate events or making sure unwanted events
+	 *  don't fire.
+	 */
+	public List<AssertStep> getCleanupAsserts()
+	{
+		return cleanupAsserts;
+	}
+	
+	/**
+	 *  Storage for this tests's result
+	 */
+	private TestResult testResult;
+	
+	/**
+	 *  This tests's result
+	 */
+	public TestResult getTestResult() 
+	{ 
+		testResult.testID = testID;
+		return testResult;
+	}
+	
+	/**
+	 * Constructor. Create the TestResult associated with this TestCase
+	 */
+	public TestCase() {
+		
+		testResult = new TestResult();
+		testResult.testID = testID;
+		
+		cleanupAsserts = new ArrayList<AssertStep>();
+	}
+	
+	/**
+	 *  Called when it is time to execute
+	 *  this test case.
+	 *
+	 */
+	public boolean runTest(WebDriver webDriver)
+	{
+		testResult.beginTime = new Date().getTime();
+						
+		return runSetup(webDriver);
+	}
+	
+	/**
+	 *  Execute the setup portion of the test
+	 */
+	private boolean runSetup(WebDriver webDriver)
+	{
+		if (!testResult.hasStatus()) 
+		{ 
+			if (setup != null)
+			{
+				testResult.phase = TestResult.SETUP;
+				currentIndex = 0;
+				currentSteps = setup;
+				numSteps = setup.size();
+				// return if we need to wait for something
+				if (!runSteps(webDriver))
+					return false;
+				
+			}
+		}
+		return runBody(webDriver);
+	}
+	
+	/**
+	 *  Execute the body portion of the test
+	 */
+	private boolean runBody(WebDriver webDriver)
+	{
+		if (!testResult.hasStatus()) 
+		{ 
+			if (body != null)
+			{
+				testResult.phase = TestResult.BODY;
+				currentIndex = 0;
+				currentSteps = body;
+				numSteps = body.size();
+				// return if we need to wait for something
+				if (!runSteps(webDriver))
+					return false;
+				
+			}
+		}
+		return runCleanup(webDriver);
+	}
+	
+	/**
+	 *  Execute the cleanup portion of the test
+	 */
+	private boolean runCleanup(WebDriver webDriver)
+	{
+		if (!testResult.hasStatus()) 
+		{ 
+			if (cleanup != null)
+			{
+				testResult.phase = TestResult.CLEANUP;
+				currentIndex = 0;
+				currentSteps = cleanup;
+				numSteps = cleanup.size();
+				// return if we need to wait for something
+				if (!runSteps(webDriver))
+					return false;
+				
+			}
+		}
+		return runComplete();
+	}
+	
+	/**
+	 *  Clean up when all three phases are done.  Sends an event
+	 *  to the UnitTester harness to tell it that it can run
+	 *  the next test case.
+	 */
+	private boolean runComplete()
+	{
+		int n = cleanupAsserts.size();
+		for (int i = 0; i < n; i++)
+		{
+			AssertStep asrt = cleanupAsserts.get(i);
+			asrt.cleanup();
+		}
+		testResult.endTime = new Date().getTime();
+		return true;
+	}
+	
+	/**
+	 *  Go through the currentSteps, executing each one.
+	 *  Returns true if no test steps required waiting.
+	 *  Returns false if we have to wait for an event before
+	 *  continuing.
+	 */
+	private boolean runSteps(WebDriver webDriver)
+	{
+		while (currentIndex < numSteps)
+		{
+			// return if a step failed
+			if (testResult.hasStatus()) 
+				return true;
+			
+			TestStep step = currentSteps.get(currentIndex);
+			if (!(step instanceof AssertStep))
+			{
+				// look at subsequent steps for Asserts and set them up early
+				for (int j = currentIndex + 1; j < numSteps; j++)
+				{
+					// scan following asserts for AssertEvents and set them up early
+					TestStep nextStep = currentSteps.get(j);
+					if (nextStep instanceof AssertStep)
+					{
+						((AssertStep)nextStep).preview(webDriver, this, testResult);
+						/* TODO: (aharui) re-enable when we need these asserts
+						// do a check to be sure folks are using AssertEventPropertyValue correctly
+						if (nextStep instanceof AssertEventPropertyValue)
+						{
+							// AEPV must follow an AssertEvent or another AEPV
+							if (j == 0 || !(currentSteps[j-1] instanceof AssertEvent || currentSteps[j-1] instanceof AssertEventPropertyValue))
+								TestOutput.logResult("WARNING: AssertEventPropertyValue may be missing preceding AssertEvent");
+						}
+						else if (nextStep instanceof AssertError)
+						{
+							if (step instanceof SetProperty)
+								SetProperty(step).expectError = true;
+						}
+						*/
+					}
+					else
+						break;
+				}
+			}
+			if (TestCase.showSteps)
+				System.out.println(step.toString());
+			step.execute(webDriver, this, testResult);
+			currentIndex++;
+		}
+		return true;
+	}
+				
+	public void populateFromAttributes(Attributes attributes)
+	{
+		description = attributes.getValue("description");
+		testID = attributes.getValue("testID");
+		keywords = attributes.getValue("keywords");
+		frequency = attributes.getValue("frequency");
+		
+	}
+	
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/TestOutput.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/TestOutput.java b/mustella/java/src/marmotinni/TestOutput.java
new file mode 100644
index 0000000..4b63f10
--- /dev/null
+++ b/mustella/java/src/marmotinni/TestOutput.java
@@ -0,0 +1,36 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+/**
+ *  The class that collects TestResults for a TestCase
+ */
+public class TestOutput 
+{
+	
+	/** 
+	 *  get printable version of phase
+	 */
+	public static void logResult(String message) { 
+		System.out.println(message);
+	}
+	
+}

http://git-wip-us.apache.org/repos/asf/flex-asjs/blob/8a08cdd2/mustella/java/src/marmotinni/TestStep.java
----------------------------------------------------------------------
diff --git a/mustella/java/src/marmotinni/TestStep.java b/mustella/java/src/marmotinni/TestStep.java
new file mode 100644
index 0000000..2019f7b
--- /dev/null
+++ b/mustella/java/src/marmotinni/TestStep.java
@@ -0,0 +1,124 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 marmotinni;
+
+import org.openqa.selenium.WebDriver;
+
+import org.xml.sax.Attributes;
+
+/** 
+ * TestStep
+ *
+ * Base class for test steps like DispatchMouseEvent and AssertPropertyValue, etc.
+ */
+public class TestStep { 
+
+	public static boolean showScripts = false;
+
+	public TestStep () { 
+	}
+
+	public boolean execute(WebDriver webDriver, TestCase testCase, TestResult testResult)
+	{
+		this.webDriver = webDriver;
+		this.testCase = testCase;
+		this.testResult = testResult;
+				
+		doStep();
+		
+		// if test failed, don't bother waiting, just bail
+		if (testResult.hasStatus())
+		{
+			if (waitEvent != null)
+			{
+				/* TODO: figure out how to wait */
+			}
+		}
+		
+		return true;
+	}
+	
+	/**
+	 *  The name of the object to listen for an event we're waiting on
+	 */
+	public String waitTarget;
+	
+	/**
+	 *  The name of the event to listen for on the waitTarget
+	 */
+	public String waitEvent;
+	
+	/**
+	 *  The number of milliseconds to wait before giving up
+	 */
+	public int timeout = 3000;
+	
+	/**
+	 *  The TestResult for this TestCase
+	 */
+	protected TestResult testResult;
+	
+	/**
+	 *  The TestCase that this step belongs to
+	 */
+	protected TestCase testCase;
+
+	/**
+	 *  The WebDriver for this session
+	 */
+	protected WebDriver webDriver;
+	
+	/**
+	 *  The method that gets called when it is time to perform the work in the step.
+	 */
+	protected void doStep()
+	{
+	}
+	
+	public void populateFromAttributes(Attributes attributes) throws Exception
+	{
+		waitTarget = attributes.getValue("waitTarget");
+		waitEvent = attributes.getValue("waitEvent");
+	}
+	
+	protected void insertTargetScript(StringBuilder sb, String target)
+	{
+		sb.append("var target = document.getElementsByTagName('body')[0];");
+		sb.append("target = target.flexjs_wrapper;");
+		sb.append("target = target.initialView;");
+		if (target == null || target.length() == 0)
+		{
+			return;
+		}
+		String parts[] = target.split("\\.");
+		int n = parts.length;
+		for (int i = 0; i < n; i++)
+		{
+			sb.append("target = target['get_' + '" + parts[i] + "']();");
+		}
+
+	}
+	
+	public String toString()
+	{
+		return "";
+	}
+}