package jp.vmi.selenium.selenese.subcommand; import java.util.HashMap; import java.util.List; import java.util.Map; import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.Point; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.interactions.MoveTargetOutOfBoundsException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jp.vmi.selenium.selenese.Context; import jp.vmi.selenium.selenese.command.ArgumentType; import static jp.vmi.selenium.selenese.command.ArgumentType.*; /** * Mouse event handler. */ public class MouseEventHandler implements ISubCommand<Void> { private static final Logger log = LoggerFactory.getLogger(MouseEventHandler.class); @SuppressWarnings("javadoc") public static enum MouseEventType { MOUSE_OVER(LOCATOR), // - MOUSE_OUT(LOCATOR), // - MOUSE_MOVE(LOCATOR), // - MOUSE_MOVE_AT(LOCATOR, VALUE), // VALUE = x,y MOUSE_DOWN(LOCATOR), // - MOUSE_DOWN_AT(LOCATOR, VALUE), // VALUE = x,y MOUSE_UP(LOCATOR), // - MOUSE_UP_AT(LOCATOR, VALUE), // VALUE = x,y ; private String commandName; private String eventName; private ArgumentType[] argTypes; private boolean hasCoord; private MouseEventType(ArgumentType... argTypes) { String[] words = name().split("_"); StringBuilder commandName = new StringBuilder(words[0].toLowerCase()); StringBuilder eventName = new StringBuilder(words[0].toLowerCase()); for (int i = 1; i < words.length; i++) { String word = words[i]; commandName.append(word.charAt(0)); if (word.length() > 1) commandName.append(word.substring(1).toLowerCase()); if (i < words.length - 1 || !word.equals("AT")) eventName.append(word.toLowerCase()); } this.commandName = commandName.toString(); this.eventName = eventName.toString(); this.argTypes = argTypes; this.hasCoord = argTypes.length == 2; } } // Use createEvent & initMouseEvent for PhantomJS. private static final String NEW_MOUSE_EVENT = "var newMouseEvent = function(element, event, init) {" + "try {" + "return new MouseEvent(event, init);" + "} catch (e) {" + "var e = document.createEvent('MouseEvents');" + "e.initMouseEvent(event, true, true, window, 0, " + "init.screenX, init.screenY, init.clientX, init.clientY, " + "init.ctrlKey, init.altKey, init.shiftKey, init.metaKey, init.button, element);" + "return e;" + "}" + "};"; private static final String FIRE_MOUSE_OVER_EVENT = "(function(element) {" + NEW_MOUSE_EVENT + "element.dispatchEvent(newMouseEvent(element, 'mouseover', {}));" + "element.dispatchEvent(newMouseEvent(element, 'mouseenter', {}));" + "})(arguments[0])"; private static final String FIRE_MOUSE_OUT_EVENT = "(function(element) {" + NEW_MOUSE_EVENT + "element.dispatchEvent(newMouseEvent(element, 'mouseleave', {}));" + "element.dispatchEvent(newMouseEvent(element, 'mouseout', {}));" + "})(arguments[0])"; private static final String FIRE_MOUSE_EVENT = "(function(element, event, init) {" + NEW_MOUSE_EVENT + "element.dispatchEvent(newMouseEvent(element, event, init));" + "}).apply(null, arguments)"; private static final int ARG_LOCATOR = 0; private static final int ARG_COORD = 1; private final MouseEventType eventType; /** * Constructor. * * @param eventType mouse event eventType. */ public MouseEventHandler(MouseEventType eventType) { this.eventType = eventType; } @Override public String getName() { return eventType.commandName; } @Override public ArgumentType[] getArgumentTypes() { return eventType.argTypes; } @SuppressWarnings("unchecked") private static <T> T eval(WebDriver driver, String script, Object... args) { return (T) ((JavascriptExecutor) driver).executeScript(script, args); } private static Point coordToPoint(String coordString) { String[] pair = coordString.trim().split("\\s*,\\s*"); int x = (int) Double.parseDouble(pair[0]); int y = (int) Double.parseDouble(pair[1]); return new Point(x, y); } private static Point calcOffset(int vpWidth, int vpHeight, Point elemLocation, Dimension elemSize) { int xOffset = elemSize.width / 2; int yOffset = elemSize.height / 2; if (elemLocation.x + elemSize.width <= 0 || elemLocation.x >= vpWidth) /* out of viewport */; else if (elemLocation.x + xOffset < 0) xOffset = 0; else if (elemLocation.x + xOffset >= vpWidth) xOffset = vpWidth - 1; if (elemLocation.y + elemSize.height <= 0 || elemLocation.y >= vpHeight) /* out of viewport */; else if (elemLocation.y + yOffset < 0) yOffset = 0; else if (elemLocation.y + yOffset >= vpHeight) yOffset = vpHeight - 1; return new Point(xOffset, yOffset); } private static Point calcOffsetOutsideElement(int vpWidth, int vpHeight, Point elemLocation, Dimension elemSize) { int xOffset, yOffset; int inside = 0; if (elemLocation.x - 1 >= 0) { xOffset = -1; } else if (elemLocation.x + elemSize.width < vpWidth) { xOffset = elemSize.width; } else { xOffset = vpWidth / 2; inside++; } if (elemLocation.y - 1 >= 0) { yOffset = -1; } else if (elemLocation.y + elemSize.height < vpHeight) { yOffset = elemSize.height; } else { yOffset = vpHeight / 2; inside++; } return (inside < 2) ? new Point(xOffset, yOffset) : null; } @Override public Void execute(Context context, String... args) { log.debug("Mouse event: {}", eventType.commandName); WebDriver driver = context.getWrappedDriver(); List<Long> viewportSize = eval(driver, "return [document.documentElement.clientWidth, document.documentElement.clientHeight];"); int vpWidth = viewportSize.get(0).intValue(); int vpHeight = viewportSize.get(1).intValue(); WebElement element = context.findElement(args[ARG_LOCATOR]); Dimension elemSize = element.getSize(); Point elemLocation = element.getLocation(); log.debug("Viewport Size: ({}, {}) / Element Location: {} / Element Size: {}", vpWidth, vpHeight, elemLocation, elemSize); Actions actions = new Actions(driver); Point coord; switch (eventType) { case MOUSE_OVER: case MOUSE_MOVE: coord = calcOffset(vpWidth, vpHeight, elemLocation, elemSize); actions.moveToElement(element, coord.x, coord.y); break; case MOUSE_OUT: coord = calcOffsetOutsideElement(vpWidth, vpHeight, elemLocation, elemSize); if (coord != null) { log.debug("Move to: ({}, {}) on {}", coord.x, coord.y, element); actions.moveToElement(element, coord.x, coord.y); } else { log.debug("Fire \"mouseleave\" and \"mouseout\" events by JS."); eval(driver, FIRE_MOUSE_OUT_EVENT, element); return null; } break; case MOUSE_MOVE_AT: coord = coordToPoint(args[ARG_COORD]); actions.moveToElement(element, coord.x, coord.y); break; case MOUSE_DOWN: coord = calcOffset(vpWidth, vpHeight, elemLocation, elemSize); actions.moveToElement(element, coord.x, coord.y).clickAndHold(); break; case MOUSE_DOWN_AT: coord = coordToPoint(args[ARG_COORD]); actions.moveToElement(element, coord.x, coord.y).clickAndHold(); break; case MOUSE_UP: coord = calcOffset(vpWidth, vpHeight, elemLocation, elemSize); actions.moveToElement(element, coord.x, coord.y).release(); break; case MOUSE_UP_AT: coord = coordToPoint(args[ARG_COORD]); actions.moveToElement(element, coord.x, coord.y).release(); break; default: throw new UnsupportedOperationException("Unsupported command: " + eventType.commandName); } try { actions.build().perform(); } catch (MoveTargetOutOfBoundsException e) { log.warn("Cannot mouse pointer move to element: {}", e.getMessage()); log.warn("Only fire \"{}\" event by JS.", eventType.eventName); if (eventType == MouseEventType.MOUSE_OVER) { eval(driver, FIRE_MOUSE_OVER_EVENT); } else if (eventType.hasCoord) { // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent Map<String, Object> init = new HashMap<>(); init.put("clientX", coord.x); init.put("clientY", coord.y); eval(driver, FIRE_MOUSE_EVENT, eventType.eventName, init); } else { eval(driver, FIRE_MOUSE_EVENT, eventType.eventName); } } return null; } }