摘要: 凌晨 2 点的救火现场,该结束了!测试工程师老王盯着满屏飘红的 Selenium 脚本陷入沉思——元素定位失效、异步加载超时、跨域页面阻塞……这已是本周第三次为 UI 自动化熬夜救火。当 UI 自动化测试成为刚需,传统工具却让团队陷入脚本脆弱、环境依赖、维护成本高的泥潭。而微软开源的 Playwright 正以革命性设计横扫测试圈,成为新一代自动化测试的事实标准。 一、为什么全球大厂都在抛弃 S……标签:Playwright, 自动化测试, LLM, 自愈测试, AI 编程分类:软件测试 → 自动化测试当 UI 自动化测试成为刚需,传统工具却让团队陷入脚本脆弱、环境依赖、维护成本高的泥潭。而微软开源的Playwright正以革命性设计横扫测试圈,成为新一代自动化测试的事实标准。一、为什么全球大厂都在抛弃 Selenium?先看两组真实对比数据:痛点Selenium 方案Playwright 方案执行速度100 用例 / 8 分钟100 用例 / 2 分钟元素定位器维护平均每周 3 小时智能定位,接近零维护跨浏览器支持需独立驱动配置开箱即用移动端测试依赖 Appium原生模拟网络拦截复杂代理配置一行代码搞定更致命的是:当页面出现动态元素时,传统方案需要这样的补救:# Selenium 的经典重试逻辑elementNonefor_inrange(10):try:elementdriver.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,testdemo.com)# iframe 内直接定位framepage.frame_locator(.payment-iframe)frame.locator(#card-number).fill(12345678)3. 移动端真机模拟精确还原移动端交互,支持传感器模拟:# 切换手机模式iphoneplaywright.devices[iPhone 13 Pro]contextbrowser.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(status200,bodyjson.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.jsconstfsrequire(fs);constpathrequire(path);constOpenAIrequire(openai);constCACHE_FILE./healing-cache.json;constCACHE_TTL_MS60*60*1000;// 1 小时constCONFIDENCE_THRESHOLD0.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){constclientnewOpenAI({apiKey:process.env.GROQ_API_KEY});constprompt定位器 ${brokenLocator} 报错:${errorMsg}当前页面 DOM 片段:${domSnapshot}请返回一个可用的 Playwright 定位器表达式(JavaScript 语法), 以及你对新定位器的置信度(0-1)。请用 JSON 格式返回: {locator: ..., confidence: 0.9, strategy: ...};constcompletionawaitclient.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){constcacheloadCache();constcachedcache[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 快照(只保留交互元素)constdomSnapshotawaitpage.evaluate((){returnArray.from(document.querySelectorAll(button, input, a, [rolebutton])).slice(0,50).map(elel.outerHTML.slice(0,200)).join(\n);});constsuggestionawaitaskLLMForLocator(originalLocator,domSnapshot,error.message);// 置信度门控:低置信度不静默通过if(!suggestion.locator||suggestion.confidenceCONFIDENCE_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_TIMEOUT3_000;asyncfunctionwithHeal(page,originalSelector,action){try{awaitpage.locator(originalSelector).waitFor({state:attached,timeout:FAST_TIMEOUT});awaitaction(page.locator(originalSelector));}catch(err){constresultawaithealLocator(page,originalSelector,err);if(!result.success||!result.newLocator)throwerr;// 把 LLM 返回的字符串 evaluate 成真正的 LocatorconsthealedLocatornewFunction(page,return${result.newLocator})(page);awaitaction(healedLocator);}}consttestbase.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_URLhttps://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[typesubmit]);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.exportsdefineConfig({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,},});四、跑起来看效果# 装依赖npminstallplaywright/test openai# 配 API key(也可以用 OpenAI / Ollama / Gemini)exportGROQ_API_KEYgsk_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)五、几条踩过的坑别静默放过低置信度的 heal。0.75 是经验值,低于它 LLM 基本在猜。让测试失败比硬过更安全。workers 设为 1。文件 cache 多 worker 并发写会写坏,要并行就换 SQLite 或 Redis。healing-cache.json 加进 .gitignore。缓存的 locator 字符串和当前 DOM 强相关,跨环境没价值,提交 healing-report.log 就行。TypeScript 项目记得加 DOM lib。page.evaluate内部document不识别,需要在 tsconfig.json 里加lib: [ES2020, DOM]。慢机器 timeout 调到 90s。3 个坏定位器连续调 LLM,30s 可能不够。六、总结自愈测试不能替代写得好的定位器,但它解决的是:当 UI 变更慢慢扩散到系统各处的时候,让你的测试套件保持绿色。并通过审计日志,把曾经坏过的 selector沉淀下来,反过来指导团队规范定位器写法。LLM 也可以替换,Ollama / Gemini / DeepSeek 都行,选你顺手的。作者:智测开发手记【智测开发手记】专注测试开发、AI 时代质量保障、Playwright 实战