playwright-拖拽验证码

一、有如下一个拖拽验证码demo,实现自动拖拽

html代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>滑动验证码</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: #f0f2f5;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.captcha-container {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.1);
  padding: 24px;
  width: 360px;
}

.captcha-title {
  font-size: 14px;
  color: #333;
  margin-bottom: 16px;
  font-weight: 500;
}

.image-wrapper {
  position: relative;
  width: 100%;
  height: 200px;
  background: #e8e8e8;
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 16px;
  user-select: none;
}

.image-wrapper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* 缺口 */
.gap {
  position: absolute;
  width: 50px;
  height: 50px;
  border-radius: 4px;
  pointer-events: none;
  box-shadow: 0 0 0 2px rgba(255,255,255,0.7), inset 0 0 0 2px rgba(255,255,255,0.7);
}

/* 滑块 */
.slider-piece {
  position: absolute;
  width: 50px;
  height: 50px;
  border-radius: 4px;
  top: 0;
  left: 0;
  cursor: grab;
  box-shadow: 0 0 0 2px #409eff, inset 0 0 0 2px #409eff;
  background: rgba(64,158,255,0.3);
  z-index: 10;
  transition: left .05s linear;
}

.slider-piece.dragging { cursor: grabbing; }

/* 底部滑轨 */
.slider-track {
  position: relative;
  width: 100%;
  height: 40px;
  background: #e8e8e8;
  border-radius: 4px;
  margin-bottom: 12px;
}

.slider-track-bg {
  position: absolute;
  top: 0; left: 0; bottom: 0;
  width: 0;
  background: linear-gradient(90deg, #409eff, #79bbff);
  border-radius: 4px 0 0 4px;
  transition: width .05s linear;
}

.slider-track-bg.success {
  background: linear-gradient(90deg, #67c23a, #95d475);
  width: 100% !important;
  border-radius: 4px;
  transition: background .3s, border-radius .3s;
}

.slider-track-bg.fail {
  background: linear-gradient(90deg, #f56c6c, #f89898);
  width: 100% !important;
  border-radius: 4px;
  transition: background .3s, border-radius .3s;
}

.slider-btn {
  position: absolute;
  top: -4px;
  left: 0;
  width: 48px;
  height: 48px;
  background: #fff;
  border: 1px solid #d9d9d9;
  border-radius: 6px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  cursor: grab;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 20;
  transition: box-shadow .2s;
}

.slider-btn:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
.slider-btn:active { cursor: grabbing; }
.slider-btn .arrow {
  display: inline-block;
  width: 0; height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 8px solid #999;
  transition: border-top-color .2s;
}
.slider-btn.active .arrow { border-top-color: #409eff; }

.slider-track.success .slider-btn {
  background: #67c23a;
  border-color: #67c23a;
}
.slider-track.success .slider-btn .arrow { border-top-color: #fff; }

.slider-track.fail .slider-btn {
  background: #f56c6c;
  border-color: #f56c6c;
}
.slider-track.fail .slider-btn .arrow { border-top-color: #fff; }

.hint {
  font-size: 12px;
  color: #999;
  text-align: center;
  min-height: 18px;
  line-height: 18px;
  transition: color .2s;
}
.hint.success { color: #67c23a; }
.hint.fail { color: #f56c6c; }
</style>
</head>
<body>

<div class="captcha-container">
  <div class="captcha-title">安全验证</div>

  <div class="image-wrapper" id="imageWrapper">
    <div class="gap" id="gap"></div>
    <div class="slider-piece" id="sliderPiece"></div>
  </div>

  <div class="slider-track" id="sliderTrack">
    <div class="slider-track-bg" id="sliderTrackBg"></div>
    <div class="slider-btn" id="sliderBtn">
      <span class="arrow"></span>
    </div>
  </div>

  <div class="hint" id="hint">请按住滑块,拖拽到缺口处</div>
</div>

<script>
(function () {
  const TRACK_WIDTH = 310;       // 滑轨可拖动像素
  const GAP_SIZE = 50;
  const TOLERANCE = 5;           // 允许误差像素

  const wrapper = document.getElementById('imageWrapper');
  const gap = document.getElementById('gap');
  const sliderPiece = document.getElementById('sliderPiece');
  const track = document.getElementById('sliderTrack');
  const bg = document.getElementById('sliderTrackBg');
  const btn = document.getElementById('sliderBtn');
  const hint = document.getElementById('hint');

  let gapX = 0;                  // 缺口 left
  let isDragging = false;
  let startX = 0;
  let currentX = 0;
  let verified = false;

  /* ---- 工具 ---- */
  function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }

  /* ---- 重置缺口与滑块位置 ---- */
  function resetChallenge() {
    const wrapperW = wrapper.clientWidth;
    gapX = rand(GAP_SIZE, wrapperW - GAP_SIZE * 2);  // 留出左右余量
    gap.style.left = gapX + 'px';
    gap.style.top = rand(0, wrapper.clientHeight - GAP_SIZE) + 'px';

    // 滑块 piece 与缺口同 Y
    sliderPiece.style.top = gap.style.top;
    sliderPiece.style.left = '0px';

    // 复位 UI
    btn.style.left = '0px';
    bg.style.width = '0px';
    bg.className = 'slider-track-bg';
    track.className = 'slider-track';
    btn.className = 'slider-btn';
    hint.textContent = '请按住滑块,拖拽到缺口处';
    hint.className = 'hint';
    verified = false;
    currentX = 0;
  }

  /* ---- 验证 ---- */
  function verify(offsetX) {
    const diff = Math.abs(offsetX - gapX);
    const passed = diff <= TOLERANCE;
    if (passed) {
      bg.className = 'slider-track-bg success';
      track.className = 'slider-track success';
      btn.className = 'slider-btn';
      hint.textContent = '验证通过';
      hint.className = 'hint success';
      // 滑块 piece 同步到最终位置
      sliderPiece.style.left = gapX + 'px';
      verified = true;
    } else {
      bg.className = 'slider-track-bg fail';
      track.className = 'slider-track fail';
      btn.className = 'slider-btn';
      hint.textContent = '验证失败,请重试';
      hint.className = 'hint fail';
      // 弹回
      setTimeout(resetChallenge, 800);
    }
  }

  /* ---- 拖动逻辑 ---- */
  function onPointerDown(e) {
    if (verified) return;
    isDragging = true;
    startX = e.clientX - currentX;
    btn.classList.add('active');
    btn.setPointerCapture(e.pointerId);
  }

  function onPointerMove(e) {
    if (!isDragging) return;
    let offset = e.clientX - startX;
    offset = Math.max(0, Math.min(offset, TRACK_WIDTH));
    currentX = offset;

    btn.style.left = offset + 'px';
    bg.style.width = offset + 'px';
    sliderPiece.style.left = offset + 'px';
  }

  function onPointerUp(e) {
    if (!isDragging) return;
    isDragging = false;
    btn.classList.remove('active');
    btn.releasePointerCapture(e.pointerId);
    if (!verified) verify(currentX);
  }

  /* ---- 事件绑定 ---- */
  btn.addEventListener('pointerdown', onPointerDown);
  btn.addEventListener('pointermove', onPointerMove);
  btn.addEventListener('pointerup', onPointerUp);
  btn.addEventListener('pointercancel', onPointerUp);

  /* ---- 阻止页面选中/拖拽图片等默认行为 ---- */
  wrapper.addEventListener('dragstart', e => e.preventDefault());

  /* ---- 初始化 ---- */
  resetChallenge();

  // 演示用:点击图片随机重置
  wrapper.addEventListener('click', () => { if (verified) resetChallenge(); });
})();
</script>
</body>
</html>

二、playwright 拖拽测试类

java 复制代码
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.BoundingBox;
import util.CaptchaSolver;
import util.MouseTracker;

public class TestCaptcha {

    public static void main(String[] args)  {
        try (Playwright playwright = Playwright.create()) {
            Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(false));
            Page page = browser.newPage();
            //打开测试页面
            page.navigate("file:///E:/OPENCODE/captcha.html");
            CaptchaSolver.solveSlider(page);


            //停2秒
            page.waitForTimeout(10000);
            browser.close();
        }


    }



}

工具类:

复制代码
CaptchaSolver
java 复制代码
package util;

import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.BoundingBox;

public class CaptchaSolver {

  public static void solveSlider(Page page) {
    String leftStr = page.evaluate("document.getElementById('gap').style.left").toString();
    double targetX = Double.parseDouble(leftStr.replace("px", ""));

    BoundingBox btnBox = page.locator("#sliderBtn").boundingBox();
    if (btnBox == null) throw new RuntimeException("Slider button not found");

    double fromX = btnBox.x + btnBox.width / 2;
    double toX = btnBox.x + btnBox.width / 2 + targetX;
    double y = btnBox.y + btnBox.height / 2;

    int steps = (int) Math.max(Math.abs(toX - fromX) / 2, 10);
    steps = Math.min(steps, 30);

    MouseTracker.drag(page, fromX, y, toX, y, steps);
  }
}

工具类

复制代码
MouseTracker
java 复制代码
package util;

import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import com.microsoft.playwright.options.BoundingBox;

public class MouseTracker {

  public static void inject(Page page) {
    page.evaluate("() => {" +
      "const dot = document.createElement('div');" +
      "dot.id = '__pw_mouse_tracker__';" +
      "dot.style.cssText = 'position:fixed;width:10px;height:10px;" +
        "background:red;border-radius:50%;z-index:99999;" +
        "pointer-events:none;transform:translate(-50%,-50%)';" +
      "document.body.appendChild(dot);" +
      "}");
  }

  public static void moveTo(Page page, double x, double y) {
    page.evaluate("(x, y) => {" +
      "const d = document.getElementById('__pw_mouse_tracker__');" +
      "if (d) { d.style.left = x + 'px'; d.style.top = y + 'px'; }" +
      "}");
    page.mouse().move(x, y);
  }

  public static void drag(Page page, double fromX, double fromY, double toX, double toY, int steps) {
    moveTo(page, fromX, fromY);
    page.mouse().down();
    for (int i = 1; i <= steps; i++) {
      double x = fromX + (toX - fromX) * i / steps;
      double y = fromY + (toY - fromY) * i / steps;
      moveTo(page, x, y);
    }
    page.mouse().up();
  }

  public static void dragSlider(Page page, String selector, double targetPercent) {
    Locator slider = page.locator(selector);
    BoundingBox box = slider.boundingBox();
    if (box == null) throw new RuntimeException("Element not found or not visible: " + selector);

    double fromX = box.x + box.width * 0.5;
    double toX = box.x + box.width * targetPercent;
    double y = box.y + box.height / 2;

    drag(page, fromX, y, toX, y, 10);
  }

  public static void remove(Page page) {
    page.evaluate("() => {" +
      "document.getElementById('__pw_mouse_tracker__')?.remove();" +
      "}");
  }
}

三、执行效果如下: