当前位置: 首页 > news >正文

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();" + "}"); } }

三、执行效果如下:

http://www.jsqmd.com/news/1099699/

相关文章:

  • LeWorldModel:基于JEPA的轻量化世界模型实践指南
  • 为什么要将 RTF 转换为 PDF?
  • 告别泰拉瑞亚原版限制:tModLoader模组开发实战手册
  • Opencv延迟优化
  • 项目包含项目源码、项目文档、数据库脚本、软件工具等资料;
  • 欧姆龙NJ系列EtherCAT总线通信常用系统状态字
  • Agibot第15000台人形机器人下线,具身AI量产加速
  • 【课程设计/毕业设计】基于 SpringBoot 的电子化招投标数据统计分析系统的设计与实现 基于 SpringBoot 的中小型企业线上招标管理平台【附源码、数据库、万字文档】
  • 【GitHub】 fastText:当“快“成为核心竞争力——从源码拆解 Facebook 的 10 亿词级 NLP 利器
  • 新版通达信多空主力拉升1主图2副1选股指标套装工具
  • 破局生物医药研发:实验数据标准化管理平台如何重塑科研新范式
  • web9使用RESTful完整项目的用户增删改查的项目代码
  • 从厨房秤到智能称重:用STM32F103和HX711打造你的第一个物联网传感器节点
  • Jmeter性能测试与SQL优化——电影收藏清单小程序获取收藏列表
  • 从零构建企业级多智能体教育辅助系统
  • 别把RAG当架构:Ontology(本体)才是Agent的业务世界
  • 数组名的隐式转换规则
  • 2026 照片恢复教程|5 种零基础恢复技巧汇总,最后一个90%人不知道!
  • FPGA加速数字孪生:GRU算法与硬件优化实践
  • 【Springboot毕设全套源码+文档】基于Java+springboot电缆行业生产管理系统的设计与实现(丰富项目+远程调试+讲解+定制)
  • 自动灌溉系统:AI 什么时候浇水,比老农还准?
  • 为什么我们需要关注线程?
  • 解密高并发视频中台:基于 Docker 容器化与 GB28181/RTSP 协议栈的边缘计算全纳架构(附源码交付)
  • tqdm进度条:让命令行程序更友好
  • SkyWalking:分布式系统的全栈监控方案
  • PTA 7-4 列车调度题解:不用队列,一个数组搞定(C语言版,含时间复杂度分析)
  • Linux的职业(要求)与虚拟机安装教程
  • MFile:不止是Minio的“管理中介”
  • Keil MDK vs ARM-GCC(arm-none-eabi-gcc)完整区别
  • Fuso:一个内网穿透工具,用 Rust 写的