blob: 507c6f188b28c70185d7865d2bc39695dd32f359 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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
{
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Timer;
import flash.utils.getQualifiedClassName;
import mx.collections.ArrayCollection;
import mx.core.IVisualElement;
import mx.core.IVisualElementContainer;
import mx.core.UIComponent;
import mx.effects.CompositeEffect;
import mx.effects.Effect;
import mx.events.EffectEvent;
import mx.geom.TransformOffsets;
import mx.states.Transition;
import spark.primitives.supportClasses.GraphicElement;
/**
*
* This class provides some APIs that can be useful for writing Mustella effects
* and transitions tests. This will be instrumental in the Catalyst matrix tests.
*
* It might be useful to think about building a set of TestStep classes that wrap
* some of this functionality.
*/
public class EffectTesting
{
// whether to seek the effect on effectStart (default: false)
public static var requestedSeek:Boolean = false;
// what time to seek to in an effect (default: NaN - seek to the end of the effect)
public static var requestedSeekTime:Number = NaN;
// the current effect being played
[Bindable] public static var currentEffect:Effect;
// the document that ready events will be dispatched from and transitions will be pulled from
private static var rootDocument:UIComponent;
// the character used to separate the elements in the expected values string
private static var elementSeparator:String = "|";
// the character used to separate the values in the expected values string
private static var propertySeparator:String = ",";
// keep track of the details of the latest comparison to ease further investigation
private static var lastResult:ArrayCollection;
/**
* Sets up for an effect test. This allows you to seek to a specific time in an effect.
*
* Call this method after the ResetComponent in your (non-transition) effects test.
*/
public static function setupEffectTest(document:Object, effect:Effect):String {
// reset the test properties
resetProperties(document);
// null check the effect
if (effect == null)
throw new Error("ERROR: You must provide a non-null effect to test.");
// set the current effect
currentEffect = effect;
// handle the effectEnd event
currentEffect.removeEventListener(EffectEvent.EFFECT_END, handleEffectEnd);
currentEffect.addEventListener(EffectEvent.EFFECT_END, handleEffectEnd);
// dispatches a setupComplete event and returns setupComplete String so you can
// use this with either an AssertMethodValue or RunCode in Mustella
rootDocument.dispatchEvent(new Event('setupComplete'));
return "setupComplete";
}
/**
* Sets up for a transitions test. This allows you to seek to a specific time in a transition.
*
* It parses all of the transitions in a document and sets up event listeners in a way that allows
* seeking to a specific time in the transition.
*
* Call this method after the ResetComponent in your transitions test.
*/
public static function setupTransitionTest(document:Object):String {
resetProperties(document);
var transitions:Array = rootDocument.transitions;
// don't manage any listeners if there aren't any transitions
if (transitions == null)
throw new Error("ERROR: document has no transitions");
// add event listeners to each transition
for each (var t:Transition in transitions){
// remove the effectStart event listener and add it again so we don't pile them up
t.effect.removeEventListener(EffectEvent.EFFECT_START, handleEffectStart);
t.effect.addEventListener(EffectEvent.EFFECT_START, handleEffectStart);
// remove the effectEnd event listener and add it again so we don't pile them up
t.effect.removeEventListener(EffectEvent.EFFECT_END, handleEffectEnd);
t.effect.addEventListener(EffectEvent.EFFECT_END, handleEffectEnd);
}
// dispatches a setupComplete event and returns setupComplete String so you can
// use this with either an AssertMethodValue or RunCode in Mustella
rootDocument.dispatchEvent(new Event('setupComplete'));
return "setupComplete";
}
/**
* Called by the setup methods to reset the properties in this class
*/
private static function resetProperties(document:Object):void {
// null checks
if (document == null)
throw new Error("ERROR: You must provide a non-null document.");
if (!(document is UIComponent))
throw new Error("ERROR: document must be a UIComponent");
// reset the rootDocument
rootDocument = document as UIComponent;
// reset the seek information
requestedSeek = false;
requestedSeekTime = NaN;
}
/**
* Called on effect start, kicks off the seek behavior if it is requested.
*/
private static function handleEffectStart(event:EffectEvent):void {
trace('effect start');
currentEffect = event.target as Effect;
// seek if it was requested
if (requestedSeek){
// wait roughly a frame then pause the effect before seeking
var timer:Timer = new Timer(0);
timer.repeatCount = 1;
timer.addEventListener(TimerEvent.TIMER, function(e:Event):void{ seekCurrentEffect(); });
timer.start();
}
}
/**
* Pauses then seeks to the position in the current effect. Fires an event when that is done.
*/
public static function seekCurrentEffect():void {
var seekTime:Number = requestedSeekTime;
var c:CompositeEffect = currentEffect as CompositeEffect;
// seek to the end if a specific seek time was not requested
if (isNaN(seekTime)){
// set the seekTime to the end of the effect
if (c){
// if its a Parallel/Sequence then use the compositeDuration that also handle startDelay
seekTime = c.compositeDuration;
} else {
// just a plain effect so use startDelay + duration
seekTime = currentEffect.startDelay + currentEffect.duration;
}
}
trace('effect seek to ' + seekTime);
// pause then seek
currentEffect.pause();
currentEffect.playheadTime = seekTime;
// dispatch a ready event on the document
rootDocument.dispatchEvent(new Event("seekAssertionReady"));
}
/**
* TODO: The inclusion of this method in the API is not fully baked.
* This method's name/signature/existance could change in the future
* when it is properly implemented.
*/
public static function seekCurrentEffectTo(time:Number):void {
currentEffect.playheadTime = time;
rootDocument.dispatchEvent(new Event("seekAssertionReady"));
}
/**
* TODO: The inclusion of this method in the API is not fully baked.
* This method's name/signature/existance could change in the future
* when it is properly implemented.
*/
public static function getCurrentEffectDuration():Number {
var c:CompositeEffect = currentEffect as CompositeEffect;
if (c){
// if its a Parallel/Sequence then use the compositeDuration that also handle startDelay
return c.compositeDuration;
} else {
// just a plain effect so use startDelay + duration
return currentEffect.startDelay + currentEffect.duration;
}
}
/**
* Resumes the current effect
*/
public static function resumeCurrentEffect():void {
trace("effect resume");
currentEffect.resume();
}
/**
* Fires an event after the effectEnd event that signifies an assertion is now valid.
*
* In a transition this gets called after the state values have been slammed in.
*/
private static function handleEffectEnd(e:EffectEvent):void {
trace('effect end');
// dispatch a ready event on the document
rootDocument.dispatchEvent(new Event("endAssertionReady"));
}
/**
* Given a root element it compares a set of properties across that element and any of its ancestors.
*
* Sample usage:
*
* assertPropertySet(test1, 'width, height', '70,22|10,10', 0)
* outputs: 'FAIL: test1.width: expected 70 +/- 0, but received 100')
*
* @param rootContainer - the root element to inspect
* @param propertyNameString - a string deliminated with a character that lists the properties to inspect
* @param expectedValuesString - a string deliminiated with a character that lists the values to expect
* @param tolerance - the amount of difference between actual and expected is allowed before failure
* @param depth - how deep to recurse in the rootContainer
*
* @return - a string of either "PASS" or "FAIL: ..." with a failure message
*/
public static function assertPropertySet(rootContainer:IVisualElement, propertyNamesString:String,
expectedValuesString:String, tolerance:Number = 0, depth:int = -1):String {
return checkPropertySet(rootContainer, false, propertyNamesString, expectedValuesString, tolerance, depth);
}
/**
* Given a root element it compares a set of properties across that element and any of its ancestors
* using the postLayoutTransformOffsets object of those elements.
*
* Sample usage:
*
* assertPostLayoutPropertySet(test1, 'rotationX, rotationY', '45,45|0,0', 0)
* outputs: 'FAIL: test1.rotationX: expected 45 +/- 0, but received 0')
*
* Use null as an expected value if postLayoutTransformOffsets is null for example:
* - properties: 'rotationX,rotationY'
* - expected string: 'null,null|null,null'
*
* @param rootContainer - the root element to inspect
* @param propertyNameString - a string deliminated with a character that lists the properties to inspect
* @param expectedValuesString - a string deliminiated with a character that lists the values to expect
* @param tolerance - the amount of difference between actual and expected is allowed before failure
* @param depth - how deep to recurse in the rootContainer
*
* @return - a string of either "PASS" or "FAIL: ..." with a failure message
*/
public static function assertPostLayoutPropertySet(rootContainer:IVisualElement, propertyNamesString:String,
expectedValuesString:String, tolerance:Number = 0, depth:int = -1):String {
return checkPropertySet(rootContainer, true, propertyNamesString, expectedValuesString, tolerance, depth);
}
/**
* Workhorse method that is exposed via the two public assert methods.
*
* Given a root element it compares a set of properties across that element and any of its ancestors
*
* @param rootContainer - the root element to inspect
* @param postLayout - whether to look at the postLayoutTransformOffsets object of an element
* @param propertyNameString - a string deliminated with a character that lists the properties to inspect
* @param expectedValuesString - a string deliminiated with a character that lists the values to expect
* @param tolerance - the amount of difference between actual and expected is allowed before failure
* @param depth - how deep to recurse in the rootContainer
*
* @return - a string of either "PASS" or "FAIL: ..." with a failure message
*
*/
private static function checkPropertySet(rootContainer:IVisualElement, postLayout:Boolean, propertyNamesString:String,
expectedValuesString:String, tolerance:Number = 0, depth:int = -1):String {
// reset the result of the last comparison
// add to this collection at any point a comparison happens
lastResult = new ArrayCollection();
// get the list of elements to inspect properties of
var elementsToInspect:Array = getElementsToInspect(rootContainer, depth);
// get the list of properties to inspect on each element
var propertyNames:Array = getPropertyNames(propertyNamesString);
// split up the expectedValue string into values for each element
var expectedElementValues:Array = expectedValuesString.split(elementSeparator);
// string that represents the reason for fail
var failString:String = "";
if (elementsToInspect.length != expectedElementValues.length){
// this will also catch existance failures, for example if an
// element is supposed to be included or excluded from a state
failString = "FAIL: number of elements (" + elementsToInspect.length + ") != number of expected elements (" + expectedElementValues.length + ")";
logResult(failString);
return failString;
}
// Go through each of the elements recursively in the rootContainer
for (var i:int = 0; i < elementsToInspect.length; i++){
var element:IVisualElement = elementsToInspect[i];
var expectedPropertyValues:Array = expectedElementValues[i].split(propertySeparator);
// check for a malformed expected string
if (propertyNames.length != expectedPropertyValues.length){
failString = "FAIL: number of properties != number of expected values for " + getElementId(element);
logResult(failString);
return failString;
}
// log that we are checking this property
logResult(getElementId(element));
// check each property value
for (var j:int = 0; j < propertyNames.length; j++){
var propertyName:String = propertyNames[j];
var e:* = expectedPropertyValues[j];
var a:*;
// First need to decide whether to grab the property values from
// the element or its postLayoutTransformOffsets
if (postLayout){
if (element.postLayoutTransformOffsets){
a = element.postLayoutTransformOffsets[propertyName];
} else {
a = null;
}
} else {
a = element[propertyName];
}
// prepare the log object for this property
var logItem:Object = new Object();
logItem.target = getElementId(element);
logItem.propertyName = propertyName;
logItem.actual = a;
logItem.expected = e;
logItem.tolerance = tolerance;
logItem.postLayout = postLayout;
logItem.depth = "TODO"; // TODO: one day might want to keep track of the depth of this item
logItem.result = "Unknown";
//
// String comparison
//
// First just check if expected == actual via a simple string comparison.
// If so then move on to the next propertyName, otherwise investigate further
// via null and number comparisons.
if (String(e) == String(a)){
// this property passed
// log the pass
logResult("PASS", logItem);
continue;
}
//
// Null comparison
//
// expected == actual == null so this is fine, continue to next propertyName
if (e == 'null' && a == null){
// log the pass
logResult("PASS", logItem);
continue;
}
// expected or actual is null, but not both (because of above) so fail
if (e == 'null' || a == null){
failString = "FAIL: " + describeFailureLocation(element, propertyName, postLayout) + ": " + a + ", but expected " + e;
// log the fail
logResult(failString, logItem);
return failString;
}
//
// Number comparison
//
// This approach assumes that it's ok treating undefined and NaN the same.
// This is because Number(undefined) gets turned into NaN, if this is a limitation
// might have to revisit this in the future.
var expectedValue:Number = Number(e);
var actualValue:Number = Number(a);
//
// NaN comparison
//
// expected == actual == NaN, so this is fine, continue to next propertyName
if (isNaN(actualValue) && isNaN(expectedValue)){
// log the pass
logResult("PASS", logItem);
continue;
}
// expected or actual is NaN, but not both (because of above) so fail
if (isNaN(actualValue) || isNaN(expectedValue)){
failString = "FAIL: " + describeFailureLocation(element, propertyName, postLayout) + ": expected " +
expectedValue + ' plus or minus ' + tolerance + ", but received " + actualValue;
// log the fail
logResult(failString, logItem);
return failString;
}
//
// Number tolerance comparison
//
// expected differs from actual by more than the tolerance so fail
if (Math.abs(actualValue - expectedValue) > tolerance){
failString = "FAIL: " + describeFailureLocation(element, propertyName, postLayout) + ": expected " +
expectedValue + ' plus or minus ' + tolerance + ", but received " + actualValue;
// log the fail
logResult(failString, logItem);
return failString;
}
// at this point the property passed
// log the pass
logResult("PASS", logItem);
}
// at this point the element passed, no need to log here
}
return "PASS";
}
/**
* Adds a result to the log.
*
* @param result - a simple string to add to the log
* @param details - an object that if not null is added to the log after setting details.result equal to the first parameter
*/
private static function logResult(result:String, details:Object = null):void {
if (details != null){
details.result = result;
lastResult.addItem(details);
} else {
lastResult.addItem(result);
}
}
/**
* Returns the log of the last assertion result
*/
public static function getLastResult():ArrayCollection {
return lastResult;
}
/**
* Generates a string that describes what property of what element has failed.
*
* ex:
* target.width
* target.postLayoutTransformOffsets.width
*/
private static function describeFailureLocation(element:IVisualElement, propertyName:String, postLayout:Boolean):String{
var output:String = "";
output += getElementId(element);
if (postLayout)
output += ".postLayoutTransformOffsets";
output += "." + propertyName;
return output;
}
/**
* Given a root element and a string of property names this returns the formatted string of
* each property value against that element and all descendants in a format that the assertion
* methods require.
*
* @param rootContainer
* @param propertyNamesString - ex: 'width, height, alpha'
* @param postLayout - set to true if you want to access the properties of the postLayoutTransformOffsets
* @param requestedDepth - the depth to recurse (-1 by default for full recursion)
*
* @return string
*/
public static function generatePropertySet(rootContainer:IVisualElement, propertyNamesString:String, postLayout:Boolean = false, requestedDepth:int = -1):String {
// get the list of elements to inspect properties of
var elementsToInspect:Array = getElementsToInspect(rootContainer, requestedDepth);
var propertyNames:Array = getPropertyNames(propertyNamesString);
var output:String = "";
// for each element
for (var i:int = 0; i < elementsToInspect.length; i++){
var e:IVisualElement = elementsToInspect[i];
// for each property
for (var j:int = 0; j < propertyNames.length; j++){
// the property name
var propertyName:String = propertyNames[j];
// concatenate the value
if (postLayout){
if (e.postLayoutTransformOffsets){
// access the value via the transform offsets
output += e.postLayoutTransformOffsets[propertyName];
} else {
// the transform offsets are null
output += "null";
}
} else {
// access the value directly
output += e[propertyName];
}
// concatenate the value separator
if (j < propertyNames.length - 1)
output += ",";
}
// concatenate the element separator
if (i < elementsToInspect.length - 1)
output += elementSeparator;
}
return output;
}
/**
* Returns an array of property names parsed from a comma separated string with
* spaces removed.
*/
private static function getPropertyNames(s:String):Array {
// strip spaces
while (s.indexOf(" ") != -1){
s = s.replace(' ','');
}
return s.split(propertySeparator);
}
/**
* Returns the id of an element, if one is not defined then it returns the class name
*/
private static function getElementId(element:IVisualElement):String {
var s:String = String(Object(element).id);
return (s != "null") ? s : flash.utils.getQualifiedClassName(element).split("::")[1];
}
/**
* Returns an array of all elements in a root element. If the element is not a
* container then it just returns an array of that element.
*/
public static function getElementsToInspect(root:IVisualElement, requestedDepth:int):Array {
var output:Array = new Array();
if (root is IVisualElementContainer){
// if its a container then recursively get all the elements to requestedDepth
output = getDescendants(root as IVisualElementContainer, requestedDepth);
} else {
// just return the element
output.push(root);
}
return output;
}
/**
* Recursively generates an array of all elements in a given container (including itself) to a requested depth
*/
private static function getDescendants(rootContainer:IVisualElementContainer, requestedDepth:int, depth:int = 0):Array{
var output:Array = new Array();
// push the container element
output.push(rootContainer);
// return if we've gone past the requested depth (and a requestedDepth of not -1)
if (requestedDepth != -1 && (depth >= requestedDepth)){
return output;
}
for (var i:int = 0; i < rootContainer.numElements; i++){
var e:IVisualElement = rootContainer.getElementAt(i);
if (e is IVisualElementContainer){
// recursively get the elements of the container
output = output.concat(getDescendants(e as IVisualElementContainer, requestedDepth, depth+1));
} else {
// push the non-container element
output.push(e);
}
}
return output;
}
}
}