playwright-拖拽验证码
一、有如下一个拖拽验证码demo,实现自动拖拽
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 拖拽测试类
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
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
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();" + "}"); } }