org.safs.selenium.util.DocumentClickCapture.java Source code

Java tutorial

Introduction

Here is the source code for org.safs.selenium.util.DocumentClickCapture.java

Source

/** 
 * Copyright (C) SAS Institute, All rights reserved.
 * General Public License: http://www.opensource.org/licenses/gpl-license.php
 **/
package org.safs.selenium.util;

/**
 * History:
 * OCT 15, 2014    (LeiWang) Add field 'target' as the event-receiver.
 * MAR 23, 2015    (LeiWang) Modify waitForClick(): wait the click event ready if we detect the event-fired.
 * MAR 31, 2015    (LeiWang) Modify getEventListener(): once received 'mousedown', set the global variable immediately
 *                                  Modify onEventFired(): Add debug information to show where the click happened.
 * JUN 04, 2015    (LeiWang) Modify run(): if the target webelement is stale, we consider click as success.
 * JUN 25, 2015    (LeiWang) Modify onEventFired(): ignore event's information to save time.
 * NOV 18, 2015    (LeiWang) Provide a way to disable the DocumentClickCapture.
 * FEB 22, 2016    (LeiWang) Modify to avoid setting the click listener the second time if the first listener is consumed.
 * FEB 29, 2016    (LeiWang) Modify to provide the ability to force receiving click-event by html-document.
 * MAR 14, 2016    (LeiWang) Log debug message for adding/removing/execution of click-listeners.
 * MAR 29, 2016    (LeiWang) Refactor to "add listener" and "start polling thread" separately, and detect the JS "Alert"
 *                       before start "polling thread" to listen.
 */
import java.util.Date;

import org.openqa.selenium.WebElement;
import org.safs.IndependantLog;
import org.safs.StringUtils;
import org.safs.selenium.webdriver.lib.SearchObject;
import org.safs.selenium.webdriver.lib.SeleniumPlusException;
import org.safs.selenium.webdriver.lib.WDLibrary;
import org.safs.tools.stringutils.StringUtilities;

/**
 * Used to intercept mouseup, mousedown, and click events at the DOM document level in order to allow 
 * clicks on the document that will NOT invoke the application's event handlers.  For example, when 
 * a ProcessContainer tool wants to investigate element properties by selecting an element with a click. 
 * <p>
 * Usage:
 * <p><pre><code>
 * DocumentClickCapture listener = new DocumentClickCapture();
 * try{
 *     MouseEvent event = listener.waitForClick(timeout_in_seconds);
 *     // do stuff with event info
 *     // use again if desired
 *     event = listener.waitForClick(timeout_in_seconds);
 *     // do stuff with event info
 * }catch(Exception x){
 *     // handle them here
 * }
 * </code></pre>
 * <p>
 * The use CAN allow the application's event handlers to see the events if the alternate constructor is used 
 * setting the allowPropogation boolean to true:
 * <p>
 * Usage:
 * <p><pre><code>
 * DocumentClickCapture listener = new DocumentClickCapture(true);
 * try{
 *     MouseEvent event = listener.waitForClick(timeout_in_seconds);
 *     // do stuff with event info
 *     // use again if desired
 *     event = listener.waitForClick(timeout_in_seconds);
 *     // do stuff with event info
 * }catch(Exception x){
 *     // handle them here
 * }
 * </code></pre>
 * @author canagl
 */
public class DocumentClickCapture implements Runnable {

    /** "safs_clickcapture" */
    public static final String LISTENER_ID_ROOT = "safs_clickcapture";

    /** in a loop, time (in milliseconds) to sleep before trying to detect the click event-fired on page another time.*/
    public static int LISTENER_LOOP_DELAY = 200;//milliseconds

    /**
     * After event-fired, default timeout (5000 milliseconds) before click event-ready (event information has be retrieved from page by javascript).
     * @see #timeoutBtwEvtFireAndReady
     */
    public static final int DEFAULT_TIMEOUT_BETWEEN_EVENT_FIRE_AND_READY = 5000;//milliseconds
    /** in a loop, default delay time (in milliseconds) to sleep before trying to detect the 
     * click event-ready (event information has be retrieved from page by javascript) another time.*/
    public static final int DEFAULT_DELAY_WAIT_READY_LOOP = 100;//milliseconds

    /**
     * Only {@link #eventFired} is set to true, this field will take effect.<br>
     * after event-fired, in a loop,
     * timeout (in milliseconds) before click event-ready (event information has be retrieved from page by javascript).
     * @see #eventFired
     * @see #waitForClick(long)
     */
    public static int timeoutBtwEvtFireAndReady = DEFAULT_TIMEOUT_BETWEEN_EVENT_FIRE_AND_READY;//milliseconds
    /**
     * in a loop, time (in milliseconds) to sleep before trying to detect the 
     * click event-ready (event information has be retrieved from page by javascript) another time.
     * @see #waitForClick(long)
     */
    public static int delayWaitReady = DEFAULT_DELAY_WAIT_READY_LOOP;//milliseconds

    /** true, will not retrieve event's information.*/
    public static final boolean DEFAULT_IGNORE_EVENT_INFORMATION = true;

    /**
     * Return the boolean value to decide if the javascript debug message will be output.<br>
     * The debug message will be output to:<br>
     * 1. The normal debug log file, by {@link JavaScriptFunctions#debug()}.<br>
     * 2. The browser's console message by javascript API console.log().<br>
     * 
     * @return boolean, If it is true, program will output debug message to debug log file and browser's console.
     * @see JavaScriptFunctions#jsDebugLogEnable
     */
    private static boolean debug() {
        return JavaScriptFunctions.jsDebugLogEnable;
    }

    /**
     * This field could be used to turn on/off the DocumentClickCapture.<br>
     * This field will ONLY affect the new instance of DocumentClickCapture. For example:<br>
     * <pre>
     * DocumentClickCapture.ENABLE_CLICK_CAPTURE = true;
     * DocumentClickCapture captureA = new DocumentClickCapture(true, target);
     * 
     * DocumentClickCapture.ENABLE_CLICK_CAPTURE = false;
     * DocumentClickCapture captureB = new DocumentClickCapture(true, target);
     * DocumentClickCapture captureC = new DocumentClickCapture(true, target);
     * 
     * captureA is enabled; while captureB and captureC will be disabled.
     * </pre>
     */
    public static boolean ENABLE_CLICK_CAPTURE = true;
    /**
     * Represent if this 'click capture' is enabled.<br>
     * This filed will be set in constructor {@link #DocumentClickCapture()} according to static field {@link #ENABLE_CLICK_CAPTURE}.<br>
     * <br>
     * If disabled, then we will not capture click event.<br>
     * 
     * @see #startListening()
     * @see #waitForClick(long)
     */
    private boolean enabled = true;

    /**
     * Normally the click-action will happen on a specific component, so our click-listener will capture only the click-event on that component.<br>
     * But sometimes, it is intentional that we want to click something that is just above, or below, or just outside the bounds of the component we have found.<br>
     * With this situation, we could never receive the click-event by the specific component, so we will enlarge the listening-area, that is we will<br>
     * use the document as the click-event-receiver.<br>
     * 
     * <b>Note:</b> This should be set before calling {@link #startListening()}.
     */
    private boolean enlargeListeningArea = false;

    /**
     * After event happened, we can retrieve event's information  and assign them to {@link #mouseEvent}.<br>
     * If this field is false, the program will retrieve the information, which will spend some time <br>
     * and cause the timeout {@link WDLibrary#timeoutWaitClick} reached and click action fails. If we don't<br>
     * really need these event's information, we should set this field to true to save time.<br>
     * @see #onEventFired()
     */
    private static boolean ignoreEventInformation = DEFAULT_IGNORE_EVENT_INFORMATION;

    /** get {@link #ignoreEventInformation}. */
    public static boolean isIgnoreEventInformation() {
        return ignoreEventInformation;
    }

    /** set {@link #ignoreEventInformation}. */
    public static void setIgnoreEventInformation(boolean ignoreEventInformation) {
        DocumentClickCapture.ignoreEventInformation = ignoreEventInformation;
    }

    private String listenerID = null;
    private String listenerFunction = null;
    private String listenerError = null;
    private String EVENT_PHASE = null;
    private String EVENT_TYPE = null;
    private String EVENT_TARGET = null;
    private String EVENT_CURRENTTARGET = null;
    private String EVENT_TIMESTAMP = null;
    @SuppressWarnings("unused")
    private String EVENT_VIEW = null;
    private String EVENT_DETAIL = null;
    private String EVENT_SCREENX = null;
    private String EVENT_SCREENY = null;
    private String EVENT_CLIENTX = null;
    private String EVENT_CLIENTY = null;
    private String EVENT_CTRLKEY = null;
    private String EVENT_SHIFTKEY = null;
    private String EVENT_ALTKEY = null;
    private String EVENT_METAKEY = null;
    private String EVENT_BUTTON = null;
    private String EVENT_RELATEDTARGET = null;

    private boolean keepRunning = false;
    /** will be set to true, if detect the javascript's event fire (a global variable has been set)
     * @see #onEventFired()
     */
    private boolean eventFired = false;
    /** will be set to true, after all values of the javascript's event have been gotten.
     * @see #onEventFired()
     */
    private boolean eventReady = false;
    private MouseEvent mouseEvent = null;
    private boolean propogate = false;
    private boolean notListening = true;

    /**
     * If {@link #startListening()} is called, then this field will be set to true so that<br>
     * {@link #waitForClick(long)} will not call {@link #addListeners(boolean)} to start<br>
     * a new listener.<br>
     */
    private boolean listenerStarted = false;

    /** 
     * The element is expected to receive the click event.<br>
     * If this field is null, the DOM's document is the receiver.<br>
     */
    private WebElement target = null;

    /**
     * Default constructor.
     * Capture mousedown, mouseup, and click event and DO NOT allow default operation or propagation of 
     * the event to the application.
     */
    public DocumentClickCapture() {
        listenerID = LISTENER_ID_ROOT;
        listenerFunction = listenerID + "Listener";
        listenerError = listenerID + "Error";
        EVENT_TYPE = listenerID + "_type";
        EVENT_PHASE = listenerID + "_eventPhase";
        EVENT_TARGET = listenerID + "_target";
        EVENT_CURRENTTARGET = listenerID + "_currentTarget";
        EVENT_TIMESTAMP = listenerID + "_timestamp";
        EVENT_VIEW = listenerID + "_view";
        EVENT_DETAIL = listenerID + "_detail";
        EVENT_SCREENX = listenerID + "_screenX";
        EVENT_SCREENY = listenerID + "_screenY";
        EVENT_CLIENTX = listenerID + "_clientX";
        EVENT_CLIENTY = listenerID + "_clientY";
        EVENT_CTRLKEY = listenerID + "_ctrlKey";
        EVENT_SHIFTKEY = listenerID + "_shiftKey";
        EVENT_ALTKEY = listenerID + "_altKey";
        EVENT_METAKEY = listenerID + "_metaKey";
        EVENT_BUTTON = listenerID + "_button";
        EVENT_RELATEDTARGET = listenerID + "_relatedTarget";

        //According the the static filed 'ENABLE_CLICK_CAPTURE' to enable or disable this click capture.
        enabled = ENABLE_CLICK_CAPTURE;
    }

    /**
     * Alternate constructor.
     * Capture mousedown, mouseup, and click event trio. 
     * Set allowPropogation = true if we want the final "click" to be handled by the application normally.
     * @see #DocumentClickCapture()
     */
    public DocumentClickCapture(boolean allowPropogation) {
        this();
        propogate = allowPropogation;
    }

    /**
     * Alternate constructor.
     * Capture mousedown, mouseup, and click event trio. 
     * Set allowPropogation = true if we want the final "click" to be handled by the application normally.<br>
     * The target is the receiver of the event. Only the event happens on the target, the attached listener<br>
     * will be trrigered; Set target to null, if user wants the window.document to be the event receiver.<br>
     * @see #DocumentClickCapture(boolean)
     */
    public DocumentClickCapture(boolean allowPropogation, WebElement target) {
        this(allowPropogation);
        this.target = target;
    }

    /**
     * Enable or disable the ClickCapture.<br>
     * It must be called before calling {@link #startListening()}.<br>
     * @param enabled
     * @see #startListening()
     * @see #waitForClick(long)
     */
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * If true, we will use the document as the click-event-receiver.<br>
     * If false, we will use the specific component as the click-event-receiver.<br>
     * @return boolean
     * @see #enlargeListeningArea
     */
    public boolean isEnlargeListeningArea() {
        return enlargeListeningArea;
    }

    /**
     * This should be called before calling {@link #startListening()}, if we want<br>
     * {@link #enlargeListeningArea} works as expected.<br>
     * @param enlargeListeningArea
     * @see #enlargeListeningArea
     */
    public void setEnlargeListeningArea(boolean enlargeListeningArea) {
        this.enlargeListeningArea = enlargeListeningArea;
    }

    /**
     * History:
     * <pre>
     * Jun 25, 2015 (LeiWang) Retrieve event's information will spend some time, if the event happens on a remote machine, it will take much longer.
     *                        Even if the click really happened and we spend a lot of time on getting the event's information, 
     *                        the timeout (default 2 seconds) will be reached and the click will be considered not happened, which is not what we want.
     *                        To save time, we will not retrieve event's information unless user set {@link #ignoreEventInformation} to false.
     * <pre>
     */
    private void onEventFired() {
        String debugmsg = StringUtils.debugmsg(false);
        setEventFired(true);
        mouseEvent = new MouseEvent(listenerID);

        if (!ignoreEventInformation) {
            String eventHappenTime = "";

            try {
                mouseEvent.EVENT_TYPE = (String) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_TYPE)));
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_PHASE = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_PHASE)))).intValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_TARGET = (WebElement) SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_TARGET));
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_TIMESTAMP = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_TIMESTAMP)))).longValue();
                eventHappenTime = StringUtilities.getTimeString(new Date(mouseEvent.EVENT_TIMESTAMP), true);
            } catch (Exception ignore) {
            }

            //PROBLEM IN FIREFOX 28 WITH SELENIUM 2.41
            //try{ mouseEvent.EVENT_VIEW = String.valueOf(SearchObject.executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_VIEW)));}catch(Exception ignore){}

            try {
                mouseEvent.EVENT_CURRENTTARGET = (WebElement) SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_CURRENTTARGET));
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_RELATEDTARGET = (WebElement) SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_RELATEDTARGET));
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_DETAIL = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_DETAIL)))).longValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_SCREENX = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_SCREENX)))).longValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_SCREENY = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_SCREENY)))).longValue();
            } catch (Exception ignore) {
            }

            try {
                WebElement target = mouseEvent.EVENT_TARGET;
                if (target == null)
                    target = mouseEvent.EVENT_CURRENTTARGET;
                IndependantLog.debug(debugmsg + " Event '" + mouseEvent.EVENT_TYPE + "' happended at "
                        + eventHappenTime + " on element '" + target.getTagName() + "'" + " at screen location ("
                        + mouseEvent.EVENT_SCREENX + "," + mouseEvent.EVENT_SCREENY + ").");
            } catch (Exception ignore) {
            }

            try {
                mouseEvent.EVENT_CLIENTX = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_CLIENTX)))).longValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_CLIENTY = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_CLIENTY)))).longValue();
            } catch (Exception ignore) {
            }

            try {
                mouseEvent.EVENT_CTRLKEY = ((Boolean) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_CTRLKEY)))).booleanValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_SHIFTKEY = ((Boolean) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_SHIFTKEY)))).booleanValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_ALTKEY = ((Boolean) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_ALTKEY)))).booleanValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_METAKEY = ((Boolean) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_METAKEY)))).booleanValue();
            } catch (Exception ignore) {
            }
            try {
                mouseEvent.EVENT_BUTTON = ((Long) (SearchObject
                        .executeScript(JavaScriptFunctions.getGlobalVariable(EVENT_BUTTON)))).intValue();
            } catch (Exception ignore) {
            }
        }

        IndependantLog.debug(debugmsg + " setting ready to true.");
        setReady(true);
    }

    /**
     * 
     * Inject the Document level event listeners if they are not already injected.<br>
     * Does nothing if the listeners are already injected.<br>
     * 
     * This method will also start the polling thread to watch the javascript variable used to monitor 'click event'<br>
     * if it is called with parameter true. The example code is listed below: <br>
     * <pre><code>
     * DocumentClickCapture listener = new DocumentClickCapture();
     * try{
     * 
     *     //Add the listener and start the polling thread automatically.
     *     listener.addListeners(true);
     *     
     *     //do click action;
     *     ...
     *     
     *     MouseEvent event = listener.waitForClick(timeout_in_seconds);
     *     
     * }catch(Exception x){
     *     // handle them here
     * }
     * </code></pre>
     * 
     * If the click action may cause JS "Alert" pops up, we should call this methods with parameter false<br>
     * and start the polling thread by calling {@link #startListening()} later. The example code is listed below: <br>
     * <pre><code>
     * DocumentClickCapture listener = new DocumentClickCapture();
     * try{
     * 
     *     //Add the listener
     *     listener.addListeners(false);
     *     
     *     //do click action;
     *     ...
     *     //Start listening
     *     listener.startListening();
     *     
     *     MouseEvent event = listener.waitForClick(timeout_in_seconds);
     *     
     * }catch(Exception x){
     *     // handle them here
     * }
     * </code></pre>
     * 
     * @param startListening boolean, If true, start polling thread automatically; 
     *                                otherwise, will not start it and user needs to start it by calling {@link #startListening()}.
     * 
     * @throws SeleniumPlusException
     * @see #waitForClick(long)
     * @see #addEventListeners()
     */
    public void addListeners(boolean startListening) throws SeleniumPlusException {
        String debugmsg = "DocumentClickCapture.addListeners ";

        if (!enabled) {
            IndependantLog.debug(debugmsg + " DocumentClickCapture has been disabled.");
            return;
        }

        if (notListening) {
            IndependantLog.info(debugmsg + " not Listening: injecting Listeners.");
            mouseEvent = null;
            setReady(false);
            setEventFired(false);
            addEventListeners(); // can throw SeleniumPlusException
            listenerStarted = false;
            if (startListening)
                startListening();
            try {
                Thread.sleep(100);
            } catch (Exception ignore) {
            }
        } else {
            IndependantLog.info(debugmsg + " already listening: NOT injecting Listeners.");
        }
    }

    /**
     * Start the polling thread to watch the javascript variable used to monitor 'click event'.<br>
     * Before starting the polling thread, this listener will be disabled if the JavaScript "Alert" is present.<br>
     * This method should be called after calling {@link #addListeners(boolean)} (<b>addListeners() is called with false</b>).<br>
     * The following is an example:<br>
     * 
     * <pre><code>
     * DocumentClickCapture listener = new DocumentClickCapture();
     * try{
     * 
     *     //Add the listener
     *     listener.addListeners(<b>false</b>);
     *     
     *     //do click action;
     *     ...
     *     //Start listening
     *     listener.startListening();
     *     
     *     MouseEvent event = listener.waitForClick(timeout_in_seconds);
     *     
     * }catch(Exception x){
     *     // handle them here
     * }
     * </code></pre>
     * 
     * 
     * @see #addListeners(boolean)
     */
    public void startListening() {
        String debugmsg = StringUtils.debugmsg(false);

        try {
            //If the JavaScript "Alert", "Confirm" or "Prompt" is present, the polling thread will throw
            //exception and cannot detect the click event, so we will disable this listener at the first time and
            //the polling thread will not be started.
            if (WDLibrary.isAlertPresent(Integer.toString(WDLibrary.timeoutCheckAlertForClick)))
                enabled = false;
        } catch (SeleniumPlusException e) {
            IndependantLog
                    .warn(debugmsg + " failed to check the presence of Alert, due to " + StringUtils.debugmsg(e));
        }

        if (enabled) {
            //set listenerStarted to true so that the method {@link #waitForClick(long)} will
            //not add&start a new click-listener.
            listenerStarted = true;
            Thread pollingThread = new Thread(this);
            pollingThread.setDaemon(true);//so that it will not block the whole program
            pollingThread.start();
        } else {
            IndependantLog.warn(
                    debugmsg + " DocumentClickCapture has been disabled, we will not start the polling thread.");
        }
    }

    /**
     * Stop the pollingThread from listening and remove the listeners.
     * Normally only called if waitForClick is going to be bypassed for some reason.
     */
    public void stopListening() {
        IndependantLog.info("DocumentClickCapture.stopListening attempting to stop Listeners.");
        listenerStarted = false;
        setRunning(false);
    }

    /**
     * 
     * @param secondsTimeout -- timeout in seconds before throwing InterruptedException
     * @return MouseEvent object with some or all of its fields filled in.  Fields can be null.
     * @throws InterruptedException if timeout has been reached.
     * @throws IllegalArgumentException if secondsTimeout is < 1.
     * @throws SeleniumPlusException if we could not add or remove eventListeners in the browser.
     * @see #addListeners(boolean) 
     */
    public MouseEvent waitForClick(long secondsTimeout)
            throws IllegalArgumentException, InterruptedException, SeleniumPlusException {
        String debugmsg = "DocumentClickCapture.waitForClick ";

        if (!enabled) {
            IndependantLog.debug(debugmsg + " DocumentClickCapture has been disabled! Return a void MouseEvent.");
            return new MouseEvent(null);
        }

        if (secondsTimeout < 1)
            throw new IllegalArgumentException(debugmsg + "secondsTimeout must be greater than 0.");
        //If we have not started the listener, then add new listener and start listening.
        //This situation should be for ProcessContainer.
        if (!listenerStarted)
            addListeners(true);

        //wait for the polling thread to be running at the most for 1000 milliseconds
        synchronized (this) {
            long timeoutForRunning = 1000;//milliseconds
            long end = System.currentTimeMillis() + timeoutForRunning;
            while (!isRunning() && System.currentTimeMillis() < end)
                wait(timeoutForRunning);
        }

        long curTime = System.currentTimeMillis();
        long endTime = curTime + (1000 * secondsTimeout);
        IndependantLog.info(debugmsg + " waiting for event fired signal.");
        while ((curTime < endTime) && isRunning() && !isEventFired()) {
            try {
                Thread.sleep(delayWaitReady);
            } catch (InterruptedException ignore) {
                IndependantLog.warn(debugmsg + " carelessly ignoring Sleep InterruptedException!?");
            }
            curTime = System.currentTimeMillis();
        }
        if (isEventFired()) {
            IndependantLog.info(debugmsg + " detected event has been fired. Checking READY.");
        } else {
            IndependantLog.info(debugmsg + " detected event has NOT been fired.");
        }
        setRunning(false);
        notListening = true;

        //If the event has been fired but not ready, then the event should be ready (event got from page by js) in a few while.
        if (!isReady() && isEventFired()) {
            int tic = 0;
            int totalTics = timeoutBtwEvtFireAndReady / delayWaitReady;
            while (!isReady() && tic++ < totalTics) {//Wait ready for more times
                try {
                    Thread.sleep(delayWaitReady);
                } catch (InterruptedException ignore) {
                }
            }
            // TODO CANAGL resetting eventFired may be wrong here?!
            // only if !isReady() ?
            setEventFired(false);
        }

        if (isReady()) {
            setReady(false);
        } else {
            IndependantLog.info(debugmsg + " isReady() timeout has been reached.");
            throw new InterruptedException(debugmsg + " isReady() timeout has been reached.");
        }
        IndependantLog.info(debugmsg + " returning mouseEvent: " + mouseEvent);
        return mouseEvent;
    }

    private synchronized boolean isRunning() {
        return keepRunning;
    }

    private synchronized void setRunning(boolean keepRunning) {
        this.keepRunning = keepRunning;
        this.notifyAll();
        //IndependantLog.debug("DocumentClickCapture.setRunning(): notify all with "+keepRunning);
    }

    private synchronized boolean isReady() {
        return eventReady;
    }

    private synchronized void setReady(boolean ready) {
        eventReady = ready;
    }

    private synchronized boolean isEventFired() {
        return eventFired;
    }

    private synchronized void setEventFired(boolean eventFired) {
        this.eventFired = eventFired;
    }

    /**
     * Internal Runnable use only.
     * <p>
     * This thread monitors the javascript event flag to be set indicating the event was fired.
     * monitor loop can be stopped internally with setRunning(false).
     * If the event is detected the onEventFired() method is invoked by this thread.
     * This is a single-fire event.  Once fired or stopped the monitor will exit and the 
     * remote EventListeners will be removed from the EventTarget.
     */
    public void run() {
        mouseEvent = null;
        setReady(false);
        setEventFired(false);
        setRunning(true);
        String debugmsg = "DocumentClickCapture.run ";
        IndependantLog.debug(debugmsg + "started pollingThread for " + listenerID);
        boolean eventfired = false;
        boolean jsGone = false;
        boolean targetGone = false;
        while (isRunning()) {
            try {
                IndependantLog.suspendLogging();
                try {
                    eventfired = SearchObject.js_getGlobalBoolVariable(listenerID);
                } catch (IllegalStateException is) {
                    // 99% likely the page has been refreshed because of the click!
                    // our global variables and such are GONE!
                    IndependantLog.resumeLogging();
                    IndependantLog.debug(debugmsg + listenerID + " global data appears to be GONE! DOM refresh?");
                    eventfired = true;
                    jsGone = true;
                } catch (Throwable thr) {
                    IndependantLog.resumeLogging();
                    IndependantLog.debug(debugmsg + listenerID + " getGlobalBoolVariable Throws "
                            + thr.getClass().getSimpleName() + ", " + thr.getMessage());
                    eventfired = true;
                    jsGone = true;
                }
                IndependantLog.resumeLogging();

                //if the target is gone, we don't need to wait for the "listener detection"
                try {
                    if (!eventfired && target != null && WDLibrary.isStale(target)) {
                        IndependantLog.debug(debugmsg + listenerID + " target appears to be GONE!");
                        eventfired = true;
                        targetGone = true;
                    }
                } catch (SeleniumPlusException se) {
                    IndependantLog.warn(debugmsg + StringUtils.debugmsg(se));
                }

                if (eventfired) {
                    IndependantLog.debug(debugmsg + "detecting and handling eventfired as true...");
                    try {
                        if (jsGone || targetGone) {
                            mouseEvent = new MouseEvent(listenerID);
                            setReady(true);
                        } else {
                            IndependantLog.debug(debugmsg + listenerID + " event has been fired.");
                            onEventFired();
                        }
                        setRunning(false);
                        //reset the global variable
                        if (!jsGone) {
                            IndependantLog.debug(debugmsg + listenerID + " resetting global javascript variable.");
                            SearchObject.js_setGlobalBoolVariable(listenerID, false);
                        } else {
                            IndependantLog.debug(
                                    debugmsg + listenerID + " javascript variable GONE and will not be reset.");
                        }
                    } catch (Throwable t) {
                        IndependantLog.debug(debugmsg + " ignoring " + t.getClass().getName() + ". " + listenerID
                                + " target may now be invalid!");
                        if (mouseEvent == null) {
                            mouseEvent = new MouseEvent(listenerID);
                        }
                        setRunning(false);
                        setReady(true);
                    }
                } else {
                    IndependantLog.debug(debugmsg + "event not yet fired...relooping.");
                    try {
                        Thread.sleep(LISTENER_LOOP_DELAY);
                    } catch (InterruptedException ignore) {
                    }
                }
            } catch (Throwable e) {
                IndependantLog.resumeLogging();
                IndependantLog
                        .error(debugmsg + " pollingThread " + e.getClass().getSimpleName() + ", " + e.getMessage());
                break;
            }
        }
        IndependantLog.resumeLogging();
        setRunning(false);
        try {
            removeEventListeners();
        } catch (Throwable ignore) {
            IndependantLog.warn(debugmsg + " ignoring failure to removeEventListeners: "
                    + ignore.getClass().getSimpleName() + ", " + ignore.getMessage());
        }
        IndependantLog.debug(debugmsg + "ended pollingThread for " + listenerID);
    }

    public String getListenerID() {
        return listenerID;
    }

    /**
     * Define a javascript EventListener that performs a Capture and only allow the 
     * mousedown, mouseup, or click events to "trickle down" to the application component(s) if allowPropogation 
     * was true during initialization.
     * <p>
     * The EventListener will set the root global variable to true when a Mouse "click" event 
     * has occurred and will fill-in associated global MouseEvent variables with the info 
     * from the event.
     * <p>
     * @param variable String, the root global variable name.
     * @return String, the javascript callback function.
     */
    private String getEventListener() {
        String captureOption = propogate ? ""
                : "        evt.preventDefault();\n" + "        evt.stopPropagation();\n";

        return "\n" + "window." + listenerID + "=false;\n" + "window." + listenerError + "='';\n" + "window."
                + listenerFunction + "={\n" + "    handleEvent: function (evt) {\n"
                + (debug()
                        ? "        try{console.log('============================    '+evt.type+' happened.  =========================');}catch(err){}\n"
                        : "")
                + "        switch(evt.type) {\n" + "            case 'mouseup':\n"
                + "                break;// ignore \n" + "            case 'mousedown':\n" +
                //When receive the event, set the global variable immediately so that 
                //even the time used for gathering event information is bigger than the timeout of waiting for 'mousedown'
                //we still can receive the 'event fired' at java side.
                "                window." + listenerID + "=true;\n" + "                try{window." + EVENT_TYPE
                + "=evt.type;}catch(err){}\n" + "                try{window." + EVENT_PHASE
                + "=evt.eventPhase;}catch(err){}\n" + "                try{window." + EVENT_TARGET
                + "=evt.target;}catch(err){}\n" +

                // PROBLEM IN FIREFOX 28 WITH SELENIUM 2.41
                //            "                try{window."+ EVENT_VIEW +"=evt.view;}catch(err){}\n"+

                "                try{window." + EVENT_RELATEDTARGET + "=evt.relatedTarget;}catch(err){}\n"
                + "                try{window." + EVENT_CURRENTTARGET + "=evt.currentTarget;}catch(err){}\n"
                + "                try{window." + EVENT_TIMESTAMP + "=evt.timeStamp;}catch(err){}\n"
                + "                try{window." + EVENT_DETAIL + "=evt.detail;}catch(err){}\n"
                + "                try{window." + EVENT_SCREENX + "=evt.screenX;}catch(err){}\n"
                + "                try{window." + EVENT_SCREENY + "=evt.screenY;}catch(err){}\n"
                + "                try{window." + EVENT_CLIENTX + "=evt.clientX;}catch(err){}\n"
                + "                try{window." + EVENT_CLIENTY + "=evt.clientY;}catch(err){}\n"
                + "                try{window." + EVENT_CTRLKEY + "=evt.ctrlKey;}catch(err){}\n"
                + "                try{window." + EVENT_SHIFTKEY + "=evt.shiftKey;}catch(err){}\n"
                + "                try{window." + EVENT_ALTKEY + "=evt.altKey;}catch(err){}\n"
                + "                try{window." + EVENT_METAKEY + "=evt.metaKey;}catch(err){}\n"
                + "                try{window." + EVENT_BUTTON + "=evt.button;}catch(err){}\n"
                + (debug()
                        ? "                try{console.log('===================================   set window."
                                + listenerID + " to ture   ===========================');}catch(err){}\n"
                        : "")
                + "                break;// ignore \n" + "            case 'click':\n"
                + "                break;// ignore \n" + "        }\n" + captureOption + "    }\n" + "};\n";
    }

    /**
     * @return true if the browser already has the listener object injected into the window object.
     */
    private boolean isSet() {
        boolean set = false;
        String debugmsg = "DocumentClickCapture.isSet executeScript ";
        String script = "if(typeof window." + listenerFunction
                + " != 'undefined'){ return true;}else{return false;}\n";
        try {
            set = ((Boolean) SearchObject.executeScript(script)).booleanValue();
        } catch (ClassCastException cc) {
            IndependantLog.debug(debugmsg + "failed to receive a Boolean result.");
        } catch (SeleniumPlusException sp) {
            IndependantLog.debug(debugmsg + "SeleniumPlusException " + sp.getMessage());
        }
        return set;
    }

    /**
     * Used to hold the component's original border style so that after highlight we can set
     * the component's border style back to normal.
     * 
     * @see #_addEventListeners()
     * @see #_removeEventListeners()
     */
    private static final String GLOBAL_VAR_BORDER_STYLE = "window.global.safs.var.borderStyle";

    /**
     * Attach event-listeners to a target.<br>
     * @param domelement (<b>Javascript</b>) Object, the dom-element to attach the event-listeners.
     * @see #addEventListeners()
     */
    String _addEventListeners() {
        String debugmsg = StringUtils.debugmsg(false);

        StringBuffer scriptCommand = new StringBuffer();

        scriptCommand.append("function _addEventListeners(domelement){\n");
        scriptCommand.append("  var target=null;\n");
        if (enlargeListeningArea) {
            scriptCommand.append("  target = window.document;\n");
        } else {
            scriptCommand.append("  if(domelement!=undefined){\n");
            scriptCommand.append("    target = domelement;\n");
            scriptCommand.append("  }else{\n");
            scriptCommand.append("    target = window.document;\n");
            scriptCommand.append("  }\n");
        }
        if (debug()) {
            scriptCommand.append("  debug(' " + debugmsg + " adding listener " + listenerID + " to ');\n");
            scriptCommand.append("  debug(target);\n");
            scriptCommand.append("  " + GLOBAL_VAR_BORDER_STYLE + "=target.style.border;\n");
            scriptCommand.append("  target.style.border='3px dashed red';\n");
        }

        scriptCommand.append(
                "  try{target.addEventListener('mouseup', window." + listenerFunction + ", true);}catch(err){}\n");
        scriptCommand.append("  try{target.addEventListener('mousedown', window." + listenerFunction
                + ", true);}catch(err){}\n");
        scriptCommand.append(
                "  try{target.addEventListener('click', window." + listenerFunction + ", true);}catch(err){}\n");
        scriptCommand.append("};\n");
        return scriptCommand.toString();
    }

    /**
     * Remove the attached event-listeners from a target.<br>
     * @param domelement (<b>Javascript</b>) Object, the dom-element to detach the event-listeners.
     * @see #removeEventListeners()
     */
    String _removeEventListeners() {
        String debugmsg = StringUtils.debugmsg(false);
        StringBuffer scriptCommand = new StringBuffer();

        scriptCommand.append("function _removeEventListeners(domelement){\n");
        scriptCommand.append("  var target=null;\n");
        if (enlargeListeningArea) {
            scriptCommand.append("  target = window.document;\n");
        } else {
            scriptCommand.append("  if(domelement!=undefined){\n");
            scriptCommand.append("    target = domelement;\n");
            scriptCommand.append("  }else{\n");
            scriptCommand.append("    target = window.document;\n");
            scriptCommand.append("  }\n");
        }
        if (debug()) {
            scriptCommand.append("  debug(' " + debugmsg + " removing listener " + listenerID + " from ');\n");
            scriptCommand.append("  debug(target);\n");
            scriptCommand.append("  target.style.border=" + GLOBAL_VAR_BORDER_STYLE + ";\n");
        }

        scriptCommand.append("  window.listenerError='';\n");
        scriptCommand.append("  try{target.removeEventListener('mouseup', window." + listenerFunction
                + ", true);}catch(err){window." + listenerError + " += err;}\n");
        scriptCommand.append("  try{target.removeEventListener('mousedown', window." + listenerFunction
                + ", true);}catch(err){window." + listenerError + " += err;}\n");
        scriptCommand.append("  try{target.removeEventListener('click', window." + listenerFunction
                + ", true);}catch(err){window." + listenerError + " += err;}\n");
        scriptCommand.append("};\n");
        return scriptCommand.toString();
    }

    /**
     * Adds/Renews the listener to mousedown, mouseup, and click events on the {@link #target}.
     * @throws SeleniumPlusException if we fail the addEventListener calls in the browser.
     */
    private void addEventListeners() throws SeleniumPlusException {
        String debugmsg = "DocumentClickCapture.addEventListeners ";
        setReady(false);
        try {
            // only add the EventListener object the first time
            // except for FireFox.  It does not retain them?
            String listener = isSet() ? "" : getEventListener();
            String script = listener + _addEventListeners() + " _addEventListeners(arguments[0]);";
            SearchObject.executeScript(script, target);
            notListening = false;

        } catch (SeleniumPlusException e1) {
            IndependantLog.error(debugmsg + " failed to add EventListeners.", e1);
            throw new SeleniumPlusException("failed to add EventListeners: " + e1.getMessage());
        }
    }

    /**
     * Removes this listener from {@link #target} for mousedown, mouseup, and click events.
     * @throws SeleniumPlusException
     */
    private void removeEventListeners() {
        String debugmsg = "DocumentClickCapture.removeEventListeners ";
        setRunning(false);
        String errmsg = "";
        try {
            // listenerSent will be true when/if addEventListeners sets it
            // if it was not set there, then we need to send it each time (Firefox?).
            String listener = isSet() ? "" : getEventListener();
            notListening = true;
            String script = listener + _removeEventListeners() + " _removeEventListeners(arguments[0]);";
            ;
            SearchObject.executeScript(script, target);
            try {
                errmsg = SearchObject.executeScript("return window." + listenerError + ";\n").toString();
            } catch (Throwable t) {
            }
            IndependantLog.info(debugmsg + " problems, if any: " + errmsg);
        } catch (Throwable x) {
            try {
                errmsg = SearchObject.executeScript("return window." + listenerError + ";\n").toString();
            } catch (Throwable t) {
            }
            IndependantLog.debug(debugmsg + " failed to remove EventListeners for " + listenerID + ", JS Error: '"
                    + errmsg + "', Java Exception: '" + StringUtils.debugmsg(x) + "'");
        }
    }
}