ST.context.Local = ST.define({
    extend: ST.context.Base,
 
    flushingMessages: false,
 
    isLocalContext: true,
 
    constructor: function (config) {
        var me = this,
            pointerElement = me.pointerElement = document.createElement('div'),
            className = 'orion-mouse-pointer';
 
        ST.context.Local.superclass.constructor.call(me, config);
 
        if (ST.isMac || ST.isiOS) {
            className += ' orion-mouse-pointer-mac';
        }
 
        ST.apply(me, config);
 
        me.futureData = {};
        pointerElement.className = className;
 
        if (ST.event.Injector) {
            me.injector = new ST.event.Injector({
                context: me,
                translate: me.eventTranslation
            });
 
            me.gestureIndicators = [];
            me.touchCount = 0;
            me.lastGestureEndTime = 0;
            me.setupGesturePublisher();
        }
 
        ST.inBrowser = true;
    },
 
    cleanup: function () {
        var me = this;
 
        if (me.visualFeedback) {
            ST.each(me.gestureIndicators, function (indicator) {
                me.hideGestureIndicator(indicator);
            });
        }
    },
 
    stop: function (resolve, reject) {
        var me = this;
 
        if (me.visualFeedback) {
            ST.defer(function () {
                me.hidePointer();
                if (resolve) {
                    resolve();
                }
            }, 1000);
        } else {
            if (resolve) {
                resolve();
            }
        }
    },
 
    attachComponentIfAvailable: function () {
        this.cmp = this.el.getComponent();
    },
 
    gestureStartEvents: {
        mousedown: 1,
        pointerdown: 1,
        touchstart: 1
    },
 
    gestureEndEvents: {
        mouseup: 1,
        pointerup: 1,
        touchend: 1,
        pointercancel: 1,
        touchcancel: 1
    },
 
    // called before adding the event to the player queue 
    // for Local we make a Playable 
    // for WebDriver we don't... since we make the Playable only in the target 
    initEvent: function (event) {
        ST.logger.debug('Local.initEvent(',event,')');
        event.context = this;
        if (!event.isPlayable) {
            var playable = this.createPlayable(event);
            ST.logger.debug('Local.initEvent() returning playable=',playable);
            return playable;
        } else {
            return event;
        }
    },
 
    callFn: function (event, done) {
        return event.fn.call(event, done);
    },
    
    ready: function (event, resolve, reject) {
        try {
            var ready = event.ready();
            if (ready) {
                resolve(ready);
            } else {
                reject();
            }
        } catch (e) {
            reject(e);
        }
    },
 
    inject: function (event, resolve, reject) {
        var me = this,
            type = event.type,
            targetEl = event.targetEl,
            relatedTargetEl = event.relatedEl;
 
        // console.log('ST.context.Local.play(',playable); 
 
        if (!me.injector) {
            throw new Error('Injector events are not supported in a webdriver remote target');
        }
 
        if (type === 'tap') {
            me.expandTap(event);
        } else if (type === 'gridcolumntrigger') {
            me.expandOverAction(event);
        } else if (type === 'menuitemover') {
            me.expandOverAction(event);
        } else if (type === 'type') {
            me.expandType(event);
        } else if (type === 'scroll') { 
            me.expandScroll(event);
        } else if (type === 'draganddrop') {
            me.expandDragAndDrop(event);
        } else if (type === 'dblclick' && ST.isModern) {
            me.expandDblClick(event);
        } else if (event.type && event.type !== 'wait') {
            me.injector.injectEvent(event, targetEl, relatedTargetEl);
 
            if (me.gestureStartEvents[type]) {
                me.touchCount++;
                if (me.visualFeedback) {
                    me.showGestureIndicator();
                }
            } else if (me.gestureEndEvents[type]) {
                me.touchCount--;
                if (me.visualFeedback) {
                    me.hideGestureIndicator();
                }
                me.lastGestureEndTime = +new Date();
            } else if (me.visualFeedback && type === 'click') {
                if (((+new Date()) - me.lastGestureEndTime) > 300) {
                    // just in case we are playing a 'click' with no preceding mousedown 
                    me.showGestureIndicator();
                    me.hideGestureIndicator(); // will hide once show animation completes 
                }
            } else if (type === 'keydown') {
                me.hidePointer();
            }
        }
 
        event.state = 'done';
 
        resolve();
 
    },
    
    expandDragAndDrop: function (event) {
        var ddEvents = [],
            dragTarget = event.args.dragTarget || event.dragTarget,
            dragX = event.args.dragX || event.dragX,
            dragY = event.args.dragY || event.dragY,
            dropTarget = event.args.dropTarget || event.dropTarget,
            dropX = event.args.dropX || event.dropX,
            dropY = event.args.dropY || event.dropY,
            dragStart = {
                type: 'pointerdown',
                target: dragTarget,
                delay: event.delay
            };
 
        if (typeof dragX !== 'undefined') { 
            dragStart.x = dragX;
        }
        if (typeof dragY !== 'undefined') {
            dragStart.y = dragY;
        }
 
        // if no dropTarget is defined, the drag target will be the target 
        if (typeof dropTarget === 'undefined') { 
            dropTarget = '@';
        }
 
        ddEvents = [
            dragStart,
            // we have to have a minimum of 1px of movement to preserve the drag action 
            { type: 'pointermove', target: dropTarget, delay: event.delay, x: dropX + 1, y: dropY },
            { type: 'pointermove', target: dropTarget, delay: event.delay, x: dropX, y: dropY },
            { type: 'pointerup', target: dropTarget, delay: event.delay, x: dropX, y: dropY }            
        ];
 
        this.player.add(0, ddEvents);
    },
    
    expandScroll: function (event) {
        var x = event.args.x || event.x,
            y = event.args.y || event.y,
            pos = event.args.pos || event.pos,
            el = event.targetEl && ST.fly(event.targetEl),
            cmp = el && el.getComponent(),
            scrollable, isScrollable;
        
        if (ST.isArray(pos) && pos.length === 2) { 
            x = pos[0];
            y = pos[1];
        }
        
        if (cmp) { 
            scrollable = cmp.getScrollable ? cmp.getScrollable() : null;
            if (scrollable) {
                // if we have a scrollable, use its scrollTo method; should be standard in 6+, so no modern check needed 
                scrollable.scrollTo(x, y, false)
            } else if (cmp.scrollBy) {
                // otherwise, fall back to common scrollBy 
                cmp.scrollBy(x, y, false);
            }
        } else {
            isScrollable = function (node) {
                if (node === null) {
                    return null;
                } else if (node === document.body) {
                    return node;
                } else if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
                    return node;
                } else {
                    return isScrollable(node.parentNode);
                }
            };
 
            if (event.targetEl) { 
                scrollable = isScrollable(event.targetEl.dom);
            }
            // the scrollable will either be the passed-in target, or the next scrollable ancestor 
            if (scrollable) {
                scrollable.scrollTop = y;
                scrollable.scrollLeft = x;
            }
        }     
    },
 
    expandTap: function (event) {
        var x = event.args.x || event.x,
            y = event.args.y || event.y,
            options = {
                metaKey: event.metaKey,
                shiftKey: event.shiftKey,
                ctrlKey: event.ctrlKey,
                button: event.args.button || event.button,
                detail: event.detail
            },
            queue = ST.gestureQueue,
            tapEvents = [],
            target = event.target,
            x,y, el, cmp;
 
        if (typeof event.args.x === 'undefined') {
            x = event.x;
        } else {
            x = event.args.x;
        }
 
        if (typeof event.args.y === 'undefined') {
            y = event.y;
        } else {
            y = event.args.y;
        }
 
        if (typeof target === 'function') {
            target = event.target();
        }
 
        // TODO: Move special click rules for menuitem (and other affected components) to actual API classes 
        el = event.targetEl && ST.fly(event.targetEl);
        cmp = el && el.getComponent();
 
        // Injecting a "tap" on a OK/Cancel button can result in the button being 
        // destroyed (see https://sencha.jira.com/browse/ORION-408). We use relative 
        // indexing on the "target" to pull the target through from the initial event 
        // so that we don't have to search for it (and fail) on subsequent events. 
 
        // event.target for tap/click used to be a Playable but now it is a function... 
        // so target: event.target won't work... better would be to use the result of event.target() if it's a function? 
 
        // we'll not enforce visibility on the pointerup/click events; in some cases, the target will be hidden by the handlers for 
        // for pointerdown 
        tapEvents = [
            { type: 'pointerdown', target: target, delay: event.delay, x: x, y: y },
            { type: 'pointerup',   target: -1, delay: 0, x: x, y: y, visible: null },
            { type: 'click',       target: -2, delay: 0, x: x, y: y, visible: null },
            // if a gesture queue is set up, we need to add a wait in case the events are asynchronous 
            // the wait() will poll for a limited time until expected event is announced 
            { type: 'wait',        target: -2, delay: 0, x: x, y: y, ready: this.isTapGestureReady }
        ];
 
        // Add any modifer keys 
        for (= 0; i < tapEvents.length; i++) {
            ST.applyIf(tapEvents[i], options);
        }
 
        if (queue) {
            queue.activate();
        }
 
        this.player.add(0, tapEvents);
    },
 
    expandDblClick: function (event) {
        var x = event.args.x || event.x,
            y = event.args.y || event.y,
            options = {
                metaKey: event.metaKey,
                shiftKey: event.shiftKey,
                ctrlKey: event.ctrlKey,
                button: event.args.button || event.button,
                detail: event.detail
            },
            queue = ST.gestureQueue,
            tapEvents = [],
            target = event.target,
            x, y, el, cmp;
 
        if (typeof event.args.x === 'undefined') {
            x = event.x;
        } else {
            x = event.args.x;
        }
 
        if (typeof event.args.y === 'undefined') {
            y = event.y;
        } else {
            y = event.args.y;
        }
 
        if (typeof target === 'function') {
            target = event.target();
        }
 
        // TODO: Move special click rules for menuitem (and other affected components) to actual API classes 
        el = event.targetEl && ST.fly(event.targetEl);
        cmp = el && el.getComponent();
 
        // we'll not enforce double clicks by expanding it to two different clicks and removing delay for EXTjs modern 
        tapEvents = [
            { type: 'pointerdown', target: target, delay: 400, x: x, y: y },
            { type: 'pointerup',   target: -1, delay: 0, x: x, y: y, visible: null },
            { type: 'click',       target: -2, delay: 0, x: x, y: y, visible: null },
            { type: 'pointerdown', target: target, delay: 0, x: x, y: y },
            { type: 'pointerup',   target: -1, delay: 0, x: x, y: y, visible: null },
            { type: 'click',       target: -2, delay: 0, x: x, y: y, visible: null },
            // if a gesture queue is set up, we need to add a wait in case the events are asynchronous 
            // the wait() will poll for a limited time until expected event is announced 
            { type: 'wait',        target: -2, delay: 0, x: x, y: y, ready: this.isTapGestureReady }
        ];
 
        // Add any modifer keys 
        for (= 0; i < tapEvents.length; i++) {
            ST.applyIf(tapEvents[i], options);
        }
 
        if (queue) {
            queue.activate();
        }
        
        this.player.add(0, tapEvents);
    },
 
    expandOverAction: function (event) {
        var x = event.args.x || event.x,
            y = event.args.y || event.y,
            target = event.target,
            x, y, events;
 
        if (typeof event.args.x === 'undefined') {
            x = event.x;
        } else {
            x = event.args.x;
        }
 
        if (typeof event.args.y === 'undefined') {
            y = event.y;
        } else {
            y = event.args.y;
        }
 
        if (typeof target === 'function') {
            target = event.target();
        }
 
        events = [
            { type: 'focus', target: target, x: x, y: y, visible: null },
            { type: 'mouseenter', target: target, x: x, y: y, visible: null },
            { type: 'mouseover', target: target, x: x, y: y, visible: null }
        ];
 
        this.player.add(0, events);
    },
 
    isTapGestureReady: function () {
        var queue = ST.gestureQueue;
 
        if (!queue) {
            return true;
        }
 
        return queue.complete(this.target.id, 'tap');
    },
 
    onEnd: function (resolve) {
        var queue = ST.gestureQueue;
 
        if (queue) {
            queue.deactivate();
        }
        if (resolve) {
            resolve();
        }
    },
 
    expandType: function (event) {
        var me = this,
            text = event.args.text || event.text,
            key = event.args.key || event.key,
            targetEl = event.targetEl,
            typingDelay = me.typingDelay,
            caret = event.args.caret || event.caret,
            target = event.future ? event.future.locator.target : event.target,
            cmp, el, fld, events, i, len;
 
        if (typeof event.target === 'string') {
            cmp = targetEl.getComponent();
            if (cmp && cmp.el.dom === targetEl.dom) {
                // if using classic toolkit, we can use getFocusEl() 
                if (ST.isClassic) {
                    el = cmp.getFocusEl();
                }
                // if not classic and the type is a textfield, we can retrieve the input from the component  
                else if (cmp.isXType('textfield')) {
                    if (cmp.getComponent) {
                        fld = cmp.getComponent();
                    } else {
                        // in 6.5, Ext.field.Text no longer extends Ext.field.Decorator, so getComponent() doesn't exist 
                        // however, the "field" component is just the component itself, so easy enough 
                        fld = cmp;
                    }
 
                    el = fld.input || fld.inputElement; // 6.2+ changed input to inputElement 
                }
                // otherwise, just fallback to the el; this will accomodate Sencha Touch, and is the default for  
                // what getFocusEl() returns in the modern toolkit 
                else {
                    el = cmp.el || cmp.element;
                }
 
                if (el) {
                    targetEl = new ST.Element(el.dom);
                }
            }
        }
 
        if (text) {
            events = [];
            len = text.length;
 
            if (caret === undefined) {
                events = [{ type: "tap", target: target, args: {}, visibility: null }];
            }
 
            for (= 0; i < len; ++i) {
                var resetValue = false;
                key = text.charAt(i);
                
                if (=== 0 && caret === undefined) {
                    resetValue = true;
                }
 
                events.push(
                    {
                        type: 'keydown',
                        target: targetEl,
                        key: key,
                        delay: typingDelay,
                        reset: resetValue
                    },{
                        type: 'keyup',
                        target: targetEl,
                        key: key,
                        delay: 0
                    }
                );
                // console.log('ST.context.Local.expandType, i=',i,', events=',events); 
            }
        } else if (key) {
            // special keys 
            events = [
                {
                    type: 'keydown',
                    target: targetEl,
                    key: key
                },{
                    type: 'keyup',
                    target: targetEl,
                    key: key,
                    delay: 0
                }
            ];
        } else {
            return;
        }
 
        events[0].delay = event.delay || me.eventDelay;
        if (caret != null) {
            events[0].caret = caret;
        }
        // console.log('expandType calling player.add(0 with events=',events); 
        ST.player().add(0,events);
    },
 
    onPointChanged: function(x, y) {
        var me = this,
            indicators = me.gestureIndicators,
            indicator;
 
        if (me.visualFeedback) {
            me.movePointer(x, y);
 
            if (me.touchCount) {
                // Currently there is no support for multi-touch playback, so we'll just move 
                // the most recent indicator around with the mouse pointer. 
                // TODO: handle multi-touch 
                indicator = indicators[indicators.length - 1];
                me.moveGestureIndicator(indicator, x, y);
            }
        }
 
        me.x = x;
        me.y = y;
    },
 
    detachPointer: function () {
        var el = this.pointerElement,
            parentNode = el && el.parentNode;
 
        if (parentNode) {
            this.pointerElement = parentNode.removeChild(el);
        }
    },
 
    movePointer: function(x, y) {
        var pointerElement = this.pointerElement;
 
        if (!pointerElement.parentNode) {
            document.body.appendChild(pointerElement);
        }
 
        pointerElement.style.display = '';
        pointerElement.style.top = y + 'px';
        pointerElement.style.left = x + 'px';
    },
 
    hidePointer: function() {
        this.pointerElement.style.display = 'none';
    },
 
    showGestureIndicator: function() {
        var me = this,
            wrap, inner, indicator;
 
        if (me.visualFeedback) {
            wrap = document.createElement('div');
            inner = document.createElement('div');
 
            wrap.appendChild(inner);
            wrap.className = 'orion-gesture-indicator-wrap';
            inner.className = 'orion-gesture-indicator';
            wrap.style.top = me.y + 'px';
            wrap.style.left = me.x + 'px';
 
            document.body.appendChild(wrap);
 
            indicator = {
                isAnimatingSize: true,
                wrap: wrap,
                inner: inner
            };
 
            // css transitions on newly created elements do not work unless we first trigger 
            // a repaint. 
            inner.offsetWidth;
 
            inner.className += ' orion-gesture-indicator-on';
 
            function end() {
                indicator.isAnimatingSize = false;
                inner.removeEventListener('transitionend', end);
            }
 
            inner.addEventListener('transitionend', end);
 
            me.gestureIndicators.push(indicator);
        }
    },
 
    hideGestureIndicator: function(indicator) {
        var me = this,
            indicators = me.gestureIndicators,
            wrap, inner;
 
        if (!indicator && indicators.length) {
            indicator = indicators[0];
        }
 
        if (indicator) {
            wrap = indicator.wrap;
            inner = wrap.firstChild;
 
            ST.Array.remove(indicators, indicator);
 
            if (indicator.isAnimatingSize) {
                // If the size animation is still underway, wait until it completes 
                // to perform the fade animation 
                function doneAnimatingSize() {
                    ST.defer(function() {
                        // css transitions do not seem to work properly when run in 
                        // immediate succession, hence the need for the slight delay here. 
                        me.hideGestureIndicator(indicator);
                    }, 10);
                    inner.removeEventListener('transitionend', doneAnimatingSize);
                }
 
                inner.addEventListener('transitionend', doneAnimatingSize);
            } else {
                inner.addEventListener('transitionend', function () {
                    // finished fade-out transition - remove from dom 
                    if (wrap.parentNode) {
                        document.body.removeChild(wrap);
                        // ensure pointer is detached 
                        me.detachPointer();
                    }
                });
 
                inner.className += ' orion-gesture-indicator-off';
            }
 
            ST.defer(function() {
                // worst case scenario - transitionend did not fire - cleanup dom 
                if (wrap.parentNode) {
                    document.body.removeChild(wrap);
                    // ensure pointer is detached 
                    me.detachPointer();
                }
            }, 900);
        }
    },
 
    moveGestureIndicator: function(indicator, x, y) {
        if (indicator) {
            var wrap = indicator.wrap;
 
            wrap.style.top = y + 'px';
            wrap.style.left = x + 'px';
        }
    },
 
    /**
     * @private
     * Applies override to gesture publisher if applicable
     */
    setupGesturePublisher: function () {
        var hasDispatcher = false,
            isAsync = true,
            publisher, gestureInstance;
 
        if (Ext && Ext.event) {
            hasDispatcher = !!Ext.event.Dispatcher;
            gestureInstance = Ext.event.publisher && Ext.event.publisher.Gesture && Ext.event.publisher.Gesture.instance;
 
            if (gestureInstance) {
                // 5.1.0+ have a gesture instance, but in 6.2.0, async is no longer a config and events are synchronous 
                isAsync = gestureInstance && gestureInstance.getAsync ? gestureInstance.getAsync() : false;
            }
 
            // 5.0.0+ 
            if (hasDispatcher) {
                publisher = Ext.event.Dispatcher.getInstance().getPublisher('gesture');
            }
            // 5.1.0-6.0.x; in 6.2.0, events are synchronous 
            if (gestureInstance && isAsync) {
                publisher = gestureInstance;
            }
        }
 
        if (publisher) {
            Ext.override(Ext.event.publisher.Gesture, {
                publish: function (eventName, target, e) {
                    var me = this,
                        queue = me.gestureQueue;
 
                    if (e.event && e.event.eventId && queue) {
                        queue.add(eventName, e.event.eventId);
                    }
 
                    me.callParent(arguments);
                }
            });
 
            // if we have a publisher, set up a gesture queue that we can interrogate later 
            publisher.gestureQueue = ST.gestureQueue = new ST.event.GestureQueue(publisher);
        }
    },
 
    /**
      * Starts the {@link ST.event.Recorder event recorder}. Once this method is called
      * all further test execution is halted.
      *
      * This method is typically injected automatically by Sencha Test Studio when using
      * its Event Recorder and is therefore rarely called directly.
      * @method startRecording
      * @member ST
      */
    startRecording: function () {
        ST.logger.trace('context.Local.startRecording');
        var me = this,
        recorder = me.recorder || (me.recorder = new ST.event.Recorder()),
                sendMessage;
 
            if (ST.options.driverConfig) {
                sendMessage = ST.defaultContext.sendMessage;
            } else {
                sendMessage = ST.sendMessage;
            }
 
            recorder.throttle = 0;
 
            try {
                recorder.on({
                        scope: ST,
                        add: function (recorder, events) {
                            sendMessage({
                                    type: 'recordedEvents',
                                    events: events
                            });
                        },
                    stop: function () {
                            sendMessage({
                                    type: 'recordingStopped'
                            });
                        },
                    start: function () {
                            sendMessage({
                                    type: 'recordingStarted'
                            });
                        }
                });
 
                    recorder.start();
            } catch (err) {
                if (recorder) {
                        console.log(recorder);
                    }
 
                    console.error(err.message || err);
                console.error(err.stack || err);
            }
    },
 
    toggleInspectEnabled: function (message) {
        return ST.inspector.Inspect.getInstance().toggleInspectEnabled(message);
    },
 
    inspectQuery: function (msg) {
        return ST.inspector.Inspect.getInstance().highlightQuery(msg.locator);
    },
 
    inspectBatch: function (msg) {
        return ST.inspector.Inspect.getInstance().batchQuery(msg.locators);
    },
 
    inspectAllProperties: function (msg) {
        var cmp = Ext.ComponentQuery.query('[id='+msg.elId+']')[0], // TODO guard against multiples? Should never happen right? Duplicate cmp ids? 
            properties = [],
            ignoreList = ['plugins', 'frameIdRegex', 'idCleanRegex', 'validIdRe'];
 
        for (var key in cmp) {
            var value = cmp[key];
 
            if (key === 'itemId' && cmp['itemId'] === cmp['id']) {
                continue;
            }
            // skip all tpl and xyzTpl 
            if (/(^tpl)|(.*Tpl)$/.test(key)) {
                continue;
            }
 
            if (value != null
                && typeof value != 'undefined'
                && !Ext.Array.contains(ignoreList, key) 
                && !Ext.isEmpty(value) 
                && !Ext.isObject(value)
                && !Ext.isArray(value)
                && !Ext.isFunction(value)) {
                    properties.push({
                        name: key,
                        value: value.toString()
                    });
            }
            //if (cmp.hasOwnProperty(key)) {} 
        }
        return properties;
    },
 
    pollInspector: function () {
        var me = this,
            inspector = ST.inspector.Inspect.getInstance(),
            data = inspector.socketEventQueue.splice(0);
 
        // if we are going to poll, maybe we missed something so grab it first 
        while (data && data.data && data.event) {
            var msg = data.data;
            msg.type = data.event;
            ST.sendMessage(msg);
            data = inspector.socketEvent.Queue.splice(0);
        }
 
        window._inspectCallback = function (data) {
            var msg = data.data;
            msg.type = data.event;
            ST.sendMessage(msg);
            me.pollInspector();
        }
    },
 
    refreshTrees: function () {
        var inspector = ST.inspector.Inspect.getInstance();
 
        inspector.componentTree = null;
 
        ST.sendMessage({
            type: 'inspectEvent',
            componentTree: JSON.stringify(inspector.getComponentTree()),
            domTree: JSON.stringify(inspector.getDomTree()),
            url: location.toString()
        });
    },
 
    /**
     * Local context we start the inspector and the poller.
     * 
     * inspectEvent messages are sent from browser -> studio reporter via bi-directional AJAX request/poll
     * in this case I can just listen for a message from studio highlightQuery and do the work.
     * 
     * For WebDriver it will be tricker as I can only really do uni-directional communication easily so will
     * have to "switch" the direction between inspector grid clicking and inspector wizard highlighting.
     */
    startInspector: function () {
        var me = this,
            sendMessage,
            inspector;
 
        if (ST.options.driverConfig) {
            sendMessage = ST.defaultContext.sendMessage;
        } else {
            sendMessage = ST.sendMessage;
        }
 
        sendMessage({type: 'recordingStarted'});
 
        inspector = ST.inspector.Inspect.getInstance();
 
        sendMessage({
            type: 'inspectEvent',
            componentTree: JSON.stringify(inspector.getComponentTree()),
            domTree: JSON.stringify(inspector.getDomTree()),
            url: location.toString()
        });
        me.pollInspector();
    },
 
    execute: function (playable, fn, restParams, resolve, reject) {
        try {
            restParams.unshift(playable.future._value());
            var result = fn.apply(playable, restParams);
            resolve(result);
        } catch (e) {
            reject(e.message || e);
        }
    },
 
    getUrl: function (fn) {
        fn(location.toString());
    },
 
    getTitle: function (fn) {
        fn(document.title);
    },
 
    url: function (url, done) {
        url = new ST.Url(url);
        done = typeof done === 'function' ? done : ST.emptyFn;
 
        if (url.isOnlyHash()) {
            location.hash = url.getHash();
        } else {
            throw new Error('For in-browser tests, ST.navigate() can only be used for fragment URLs, not for full redirection.');
        }
 
        done();
    },
    
    setViewportSize: function (width, height, done) {
        ST.system.setViewportSize(width, height, done);
    },
 
    /**
     * Takes a snapshot of the viewport and compares it to the associated baseline image.
     * @param {String} name 
     * @param {Function} done 
     * @method screenshot
     * @member ST
     */
    _screenshot: function (name, done) {
        ST.system.screenshot({
            name: name
        }, function (comparison, err) {
            done(err, comparison);
        });
    },
 
    _checkGlobalLeaks: function (done) {
        var result = { results: [], addedGlobals: [] };
 
        try {
            result = ST.checkGlobalLeaks();
        } catch (e) {
            result.results.push({
                passed: false,
                message: 'Global leaks check threw an error ' + (&& e.message) || e
            });
        }
        done(result);
    }
    
});