news 2026/7/12 2:40:17

Playwright 自愈测试实战:让 LLM 帮你修定位器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright 自愈测试实战:让 LLM 帮你修定位器

摘要: 凌晨 2 点的救火现场,该结束了!测试工程师老王盯着满屏飘红的 Selenium 脚本陷入沉思——元素定位失效、异步加载超时、跨域页面阻塞……这已是本周第三次为 UI 自动化熬夜救火。当 UI 自动化测试成为刚需,传统工具却让团队陷入脚本脆弱、环境依赖、维护成本高的泥潭。而微软开源的 Playwright 正以革命性设计横扫测试圈,成为新一代自动化测试的事实标准。 一、为什么全球大厂都在抛弃 S……

标签:Playwright, 自动化测试, LLM, 自愈测试, AI 编程

分类:软件测试 → 自动化测试


当 UI 自动化测试成为刚需,传统工具却让团队陷入脚本脆弱、环境依赖、维护成本高的泥潭。而微软开源的Playwright正以革命性设计横扫测试圈,成为新一代自动化测试的事实标准。

一、为什么全球大厂都在抛弃 Selenium?

先看两组真实对比数据:

痛点Selenium 方案Playwright 方案
执行速度100 用例 / 8 分钟100 用例 / 2 分钟
元素定位器维护平均每周 3 小时智能定位,接近零维护
跨浏览器支持需独立驱动配置开箱即用
移动端测试依赖 Appium原生模拟
网络拦截复杂代理配置一行代码搞定

更致命的是:当页面出现动态元素时,传统方案需要这样的补救:

# Selenium 的经典重试逻辑element=Nonefor_inrange(10):try:element=driver.find_element(By.XPATH,"//div[contains(@class,'loading')]")breakexceptNoSuchElementException:time.sleep(1)

二、Playwright 的四大杀手锏

1. 智能等待:告别 sleep 噩梦

自动感知页面加载状态,无需手动等待:

# 等元素出现再操作(传统方案需显式等待)page.click("text=立即购买")# 等导航完成再继续(告别随机超时)withpage.expect_navigation():page.click("#submit-btn")

2. 跨域页面无缝操作

原生支持多 Tab 页 / iframe 交互,无需切换上下文:

# 跨域 Tab 页操作withpage.context.expect_page()asnew_tab:page.click("a[target='_blank']")new_tab.value.fill("#email","test@demo.com")# iframe 内直接定位frame=page.frame_locator(".payment-iframe")frame.locator("#card-number").fill("12345678")

3. 移动端真机模拟

精确还原移动端交互,支持传感器模拟:

# 切换手机模式iphone=playwright.devices["iPhone 13 Pro"]context=browser.new_context(**iphone)# 模拟横屏 / 地理定位 / 陀螺仪context.set_geolocation({"latitude":39.9,"longitude":116.4})context.set_orientation("landscape")

4. 网络精准拦截

控制请求与响应,实现自动化测试的终极武器:

# 拦截 API 请求page.route("**/api/userinfo",lambdaroute:route.fulfill(status=200,body=json.dumps({"name":"测试用户"})))# 捕获网络响应withpage.expect_response("**/api/checkout")asresponse:page.click("#pay-button")print(response.value.json())# 获取接口返回数据

三、实战:用 LLM 给 Playwright 加自愈能力

Playwright 虽强,但定位器还是会因为前端重构而失效。怎么办?让 LLM 来帮你修。

核心思路:定位器失效 → 抓 DOM 快照 → 调 LLM 重写定位器 → 缓存结果。

3.1 Healer 核心:src/healer.js

constfs=require('fs');constpath=require('path');constOpenAI=require('openai');constCACHE_FILE='./healing-cache.json';constCACHE_TTL_MS=60*60*1000;// 1 小时constCONFIDENCE_THRESHOLD=0.75;// 持久化 cachefunctionloadCache(){if(!fs.existsSync(CACHE_FILE))return{};returnJSON.parse(fs.readFileSync(CACHE_FILE,'utf-8'));}functionsaveCache(cache){fs.writeFileSync(CACHE_FILE,JSON.stringify(cache,null,2));}// 让 LLM 重新生成定位器asyncfunctionaskLLMForLocator(brokenLocator,domSnapshot,errorMsg){constclient=newOpenAI({apiKey:process.env.GROQ_API_KEY});constprompt=`定位器 "${brokenLocator}" 报错:${errorMsg}当前页面 DOM 片段:${domSnapshot}请返回一个可用的 Playwright 定位器表达式(JavaScript 语法), 以及你对新定位器的置信度(0-1)。请用 JSON 格式返回: {"locator": "...", "confidence": 0.9, "strategy": "..."}`;constcompletion=awaitclient.chat.completions.create({model:'llama-3.1-70b-versatile',messages:[{role:'user',content:prompt}],temperature:0.1,max_tokens:200,response_format:{type:'json_object'},});returnJSON.parse(completion.choices[0]?.message?.content??'{}');}// 主函数asyncfunctionhealLocator(page,originalLocator,error){constcache=loadCache();constcached=cache[originalLocator];// 命中缓存直接返回if(cached&&(Date.now()-cached.timestamp)<CACHE_TTL_MS){console.log(`[self-heal] ✅ Cache hit: "${originalLocator}" → "${cached.newLocator}"`);return{success:true,newLocator:cached.newLocator,confidence:cached.confidence,strategy:'cache'};}// 抓 DOM 快照(只保留交互元素)constdomSnapshot=awaitpage.evaluate(()=>{returnArray.from(document.querySelectorAll('button, input, a, [role="button"]')).slice(0,50).map(el=>el.outerHTML.slice(0,200)).join('\n');});constsuggestion=awaitaskLLMForLocator(originalLocator,domSnapshot,error.message);// 置信度门控:低置信度不静默通过if(!suggestion.locator||suggestion.confidence<CONFIDENCE_THRESHOLD){console.warn(`[self-heal] ⚠️ Low confidence (${suggestion.confidence}). Skipping.`);return{success:false,newLocator:null,confidence:suggestion.confidence,strategy:suggestion.strategy};}// 写缓存 + 审计日志cache[originalLocator]={newLocator:suggestion.locator,confidence:suggestion.confidence,timestamp:Date.now(),};saveCache(cache);fs.appendFileSync('./healing-report.log',`[${newDate().toISOString()}] HEALED: "${originalLocator}" → "${suggestion.locator}" (${suggestion.confidence})\n`);return{success:true,newLocator:suggestion.locator,confidence:suggestion.confidence,strategy:suggestion.strategy};}module.exports={healLocator};

3.2 Fixture:把每个动作都包上自愈

const{test:base}=require('@playwright/test');const{healLocator}=require('./healer');constFAST_TIMEOUT=3_000;asyncfunctionwithHeal(page,originalSelector,action){try{awaitpage.locator(originalSelector).waitFor({state:'attached',timeout:FAST_TIMEOUT});awaitaction(page.locator(originalSelector));}catch(err){constresult=awaithealLocator(page,originalSelector,err);if(!result.success||!result.newLocator)throwerr;// 把 LLM 返回的字符串 evaluate 成真正的 LocatorconsthealedLocator=newFunction('page',`return${result.newLocator}`)(page);awaitaction(healedLocator);}}consttest=base.extend({healPage:async({page},use)=>{awaituse({click:(selector)=>withHeal(page,selector,(loc)=>loc.click()),fill:(selector,value)=>withHeal(page,selector,(loc)=>loc.fill(value)),selectOption:(selector,value)=>withHeal(page,selector,async(loc)=>{awaitloc.selectOption(value);}),check:(selector)=>withHeal(page,selector,(loc)=>loc.check()),});},});module.exports={test};

3.3 测试用例

const{test,expect}=require('../src/fixtures');constBASE_URL='https://the-internet.herokuapp.com/login';// TC-01:正常定位器test('TC-01 | Login with correct locators (baseline)',async({page,healPage})=>{awaitpage.goto(BASE_URL);awaithealPage.fill('#username','tomsmith');awaithealPage.fill('#password','SuperSecretPassword!');awaithealPage.click('button[type="submit"]');awaitexpect(page.getByText('You logged into a secure area!')).toBeVisible();});// TC-02:故意写错定位器,触发 LLM 自愈test('TC-02 | Login with BROKEN locators (self-heal triggered)',async({page,healPage})=>{awaitpage.goto(BASE_URL);awaithealPage.fill('#user-name-input','tomsmith');// 错的awaithealPage.fill('#pass-word-field','SuperSecretPassword!');// 错的awaithealPage.click('#login-submit-btn');// 错的awaitexpect(page.getByText('You logged into a secure area!')).toBeVisible();});// TC-03:同样的错误,这次走缓存test('TC-03 | Second run — healer reads from cache',async({page,healPage})=>{awaitpage.goto(BASE_URL);awaithealPage.fill('#user-name-input','tomsmith');awaithealPage.fill('#pass-word-field','SuperSecretPassword!');awaithealPage.click('#login-submit-btn');awaitexpect(page.getByText('You logged into a secure area!')).toBeVisible();});

3.4 Playwright 配置

// playwright.config.jsmodule.exports=defineConfig({testDir:'./tests',timeout:90_000,// 30s 不够:3 个坏定位器 × LLM 延迟retries:0,// 重试交给 healer,不是 Playwrightworkers:1,// 文件 cache 不能并发写reporter:[['list'],['html',{outputFolder:'playwright-report',open:'never',port:9324}],],use:{headless:true,screenshot:'only-on-failure',video:'retain-on-failure',},});

四、跑起来看效果

# 装依赖npminstall@playwright/test openai# 配 API key(也可以用 OpenAI / Ollama / Gemini)exportGROQ_API_KEY=gsk_xxxxxxxxxxxxxxxxxx# 跑测试npx playwrighttest

首次运行的预期输出:

[chromium] › TC-01 | Login with correct locators 1.2s [chromium] › TC-02 | Login with BROKEN locators [self-heal] 📞 Locator failed: "#user-name-input". Calling LLM... [self-heal] ✅ Healed → page.getByLabel('Username') (confidence: 0.94) [self-heal] 📞 Locator failed: "#pass-word-field". Calling LLM... [self-heal] ✅ Healed → page.getByLabel('Password') (confidence: 0.96) [self-heal] 📞 Locator failed: "#login-submit-btn". Calling LLM... [self-heal] ✅ Healed → page.getByRole('button', { name: 'Login' }) (confidence: 0.91) 7.4s [chromium] › TC-03 | Second run — cache hit 1.8s [chromium] › TC-04 | Login fails with wrong password 1.1s 4 passed (11.5s)

五、几条踩过的坑

  1. 别静默放过低置信度的 heal。0.75 是经验值,低于它 LLM 基本在猜。让测试失败比硬过更安全。
  2. workers 设为 1。文件 cache 多 worker 并发写会写坏,要并行就换 SQLite 或 Redis。
  3. healing-cache.json 加进 .gitignore。缓存的 locator 字符串和当前 DOM 强相关,跨环境没价值,提交 healing-report.log 就行。
  4. TypeScript 项目记得加 DOM libpage.evaluate内部document不识别,需要在 tsconfig.json 里加"lib": ["ES2020", "DOM"]
  5. 慢机器 timeout 调到 90s。3 个坏定位器连续调 LLM,30s 可能不够。

六、总结

自愈测试不能替代写得好的定位器,但它解决的是:当 UI 变更慢慢扩散到系统各处的时候,让你的测试套件保持绿色

并通过审计日志,把"曾经坏过的 selector"沉淀下来,反过来指导团队规范定位器写法。

LLM 也可以替换,Ollama / Gemini / DeepSeek 都行,选你顺手的。


作者:智测开发手记
【智测开发手记】专注测试开发、AI 时代质量保障、Playwright 实战


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/12 2:40:10

三平面:端到端自动驾驶的高效三维token化新范式

1. 为什么“三平面”突然成了端到端驾驶的破局钥匙&#xff1f;最近在几个自动驾驶算法组的内部分享会上&#xff0c;我反复听到一个词被拎出来重点讨论&#xff1a;三平面&#xff08;Tri-Plane&#xff09;。不是传统BEV&#xff08;鸟瞰图&#xff09;里那个被画了十年的二维…

作者头像 李华
网站建设 2026/7/12 2:37:01

C++视觉算法优化实战:8大手法提升图像处理性能

1. 项目概述&#xff1a;为什么C优化是视觉算法的命脉&#xff1f;如果你是一名视觉算法工程师&#xff0c;或者正在向这个方向发展&#xff0c;你一定经历过这样的场景&#xff1a;算法模型在论文里跑分很高&#xff0c;但一到实际工程部署&#xff0c;尤其是在资源受限的嵌入…

作者头像 李华
网站建设 2026/7/12 2:35:42

DeepSeek-VL多模态模型实战:从原理到部署的完整指南

在业务迭代中集成多模态能力时&#xff0c;开发者常面临模型体积庞大、推理成本高、真实场景适配性差等痛点。DeepSeek-VL作为首个面向真实世界的开源视觉语言模型&#xff0c;在SEEDBench评测中逼近GPT-4V表现&#xff0c;为实际应用提供了轻量化且高效的解决方案。本文将完整…

作者头像 李华
网站建设 2026/7/12 2:34:34

STM32驱动蜂鸣器:PWM控制与硬件设计详解

1. 项目概述&#xff1a;STM32与蜂鸣器的完美结合在嵌入式系统开发中&#xff0c;声音反馈是提升用户体验的重要元素。本项目使用STM32F723IE微控制器驱动CMT-8540S-SMT贴片蜂鸣器&#xff0c;通过PWM技术实现灵活的声音控制方案。这种组合特别适合需要紧凑型声音提示的智能硬件…

作者头像 李华