场景说明

一般来说用户不太会碰到这个问题,但具备如下场景的,都需要设置移动鼠标的耗时,移动鼠标到某控件到这某位置过快,导致无法让想要的效果出现。

比较抽象,举个例子,Selenium中Actions有个方法叫dragAndDrop,用来拖动一个控件到某个偏移位置或者到另外一个控件,默认情况下,拖拽会在100毫秒内完成,所有的移动鼠标的行为都会经过Actions下的这个方法:

  private Actions moveInTicks(WebElement target, int xOffset, int yOffset) {
    return tick(defaultMouse.createPointerMove(
        Duration.ofMillis(100),
        Origin.fromElement(target),
        xOffset,
        yOffset));
  }

比较坑的是,Actions的API并没有让用户自主去设定这个耗时,而是写死的100毫秒,包括如下公开方法都收到影响:dragAndDropmoveToElement

被否决的不够好的解决方案

最开始我想到的办法是,继承Actions,重写里面的部分方法,但我发现里面需要的鼠标等对象都是私有的,moveInTicks也是私有的。

  private final static Logger LOG = Logger.getLogger(Actions.class.getName());
  private final WebDriver driver;

  // W3C
  private final Map<InputSource, Sequence> sequences = new HashMap<>();
  private final PointerInput defaultMouse = new PointerInput(MOUSE, "default mouse");
  private final KeyInput defaultKeyboard = new KeyInput("default keyboard");

  // JSON-wire protocol
  private final Keyboard jsonKeyboard;
  private final Mouse jsonMouse;
  protected CompositeAction action = new CompositeAction();

后来我看这个Actions的依赖都比较独立,干脆就全部拷贝过来,然鹅,类中有一个对Sequence的依赖的size方法不是公开的,这意味着,我只能把MyActions这个类放到和Sequence同包下,或者通过反射来调用size方法。

突然发现,解决这个耗时问题,改动过于笨重了。

最佳解决方案

在SO网站上发现这样一种说法,说actions.dragAndDrop其实就等于:

actions.clickAndHold(element1).moveToElement(elemtent2).release()

而moveToElement这个方法的实现,比较简单:

  /**
   * Moves the mouse to the middle of the element. The element is scrolled into view and its
   * location is calculated using getBoundingClientRect.
   * @param target element to move to.
   * @return A self reference.
   */
  public Actions moveToElement(WebElement target) {
    if (isBuildingActions()) {
      action.addAction(new MoveMouseAction(jsonMouse, (Locatable) target));
    }

    return moveInTicks(target, 0, 0);
  }

我们第一步调用了clickAndHold,已经把鼠标移动到第一个元素了,所以我们就不需要这句话了:action.addAction(new MoveMouseAction(jsonMouse, (Locatable) target));,有点重复。

剩下的我们只要自己实现moveInTicks就行,幸好,我们发现tick是public方法,我们这样去实现:

Actions actions = new Actions(getWebDriver());
PointerInput defaultMouse = new PointerInput(MOUSE, "default mouse");
// 执行拖拽
actions.clickAndHold(startElement)
    .tick(defaultMouse.createPointerMove(
        Duration.ofMillis(2000),
        PointerInput.Origin.fromElement(endElement), 0, 0))
    .release(endElement)
    .perform();

然而执行起来,我们会发现一个错误:

invalid argument: 'id' already exists

思考了半天,发现PointerInput在创建的时候,这个name如果为空,会随机生成一个uuid的唯一名称:

  public PointerInput(Kind kind, String name) {
    this.kind = Objects.requireNonNull(kind, "Must set kind of pointer device");
    this.name = Optional.ofNullable(name).orElse(UUID.randomUUID().toString());
  }

那么我有理由猜测,这个名称是不允许重名的,我们知道actions创建后,会默认创建一个default mouse名称的鼠标,所以我们这里不能再用这个名字了,代码改下:

Actions actions = new Actions(getWebDriver());
PointerInput defaultMouse = new PointerInput(MOUSE, "my mouse");
// 执行拖拽
actions.clickAndHold(startElement)
    .tick(defaultMouse.createPointerMove(
        Duration.ofMillis(2000),
        PointerInput.Origin.fromElement(endElement), 0, 0))
    .release(endElement)
    .perform();

完美解决!