Midscene + Playwright 定位兜底方案

张开发
2026/4/19 11:42:12 15 分钟阅读

分享文章

Midscene + Playwright 定位兜底方案
Midscene Playwright 定位兜底方案思路先用传统 Playwright 跑量用报告/flake 统计标出「经常失败」的步骤仅对这些步骤在 Playwright 重试仍失败后调用 Midscene如PlaywrightAgent.aiAct。其余步骤不走 AI控制 Token 与耗时。单步执行顺序见下图。是否否是是否步骤开始Playwright 主路径成功?下一步重试已用尽?Midscene 执行自然语言目标成功?失败示例代码TypeScript依赖版本按项目与官方文档调整# 安装 Midscene 的 Playwright 集成、Playwright 本体、以及测试运行器版本号按项目锁定npmi midscene/web playwright playwright/test下面为可单独保存的pw-midscene-fallback.ts思路封装「先 PW、再 AI」、配额与白名单。runMidscene内使用官方PlaywrightAgent若你方 API 有差异只改这一处即可。/** * Playwright 主定位失败后的 Midscene 兜底示例 * 依赖: midscene/web/playwright, playwright/test仅用到 Page 类型时可只装 playwright */importtype{Page}fromplaywright/test;import{PlaywrightAgent}frommidscene/web/playwright;/** 本步最终是由纯 Playwright 完成还是中途启用了 Midscene */exporttypeFallbackRoutepw|midscene;/** 单步执行结束时的摘要便于打日志或上报看板 */exportinterfaceStepResult{/** 实际走通的路径 */route:FallbackRoute;/** 与 opts.stepId 一致方便关联「哪一步触发了兜底」 */stepId:string;/** 预留字段若以后想返回警告而非抛错可在此携带 */error?:Error;}/** * Midscene 调用次数配额防止单用例/单 job 在异常页面上无限打模型。 * 全 job 共享时可把实例挂到 globalSetup 或单例模块上。 */exportclassMidsceneQuota{/** * param maxPerTest 每个 test或每个你传入的「作用域」最多消耗几次 AI * param used 已用次数一般从 0 开始单测里也可手动注入做断言 */constructor(privatemaxPerTest:number,privateused0,){}/** * 尝试占用 1 次配额。 * returns true 表示配额扣减成功可以调 Midscenefalse 表示已达上限禁止再调 */tryConsume():boolean{if(this.usedthis.maxPerTest)returnfalse;this.used1;returntrue;}/** 当前还剩几次可调用只读便于调试打印 */getremaining():number{returnMath.max(0,this.maxPerTest-this.used);}}/** 传给 withPwOrMidsceneFallback 的完整配置 */exportinterfaceFallbackOptions{/** 当前页面对象Playwright 与 Midscene 共用同一上下文 */page:Page;/** * 当前原子步骤的标识字符串需与白名单里的某一项完全一致才会走 AI。 * 命名建议模块_动作如 login_submit。 */stepId:string;/** 仅当 stepId 在此集合中才允许走 Midscene其余步骤 PW 失败后直接抛错 */aiWhitelist:ReadonlySetstring;/** 与本 test 绑定的配额计数器 */quota:MidsceneQuota;/** * Playwright 路径的「额外重试次数」总尝试次数 1 pwRetries。 * 例如 pwRetries2 表示最多共 3 次 pwAttempt。 */pwRetries:number;/** 两次 pwAttempt 之间的间隔给动画、请求、DOM 稳定一点时间 */pwRetryDelayMs?:number;/** 交给 Midscene 的自然语言指令越短、越贴近界面文案越好 */midsceneGoal:string;}/** 简单延迟避免 tight loop 连续打页面 */asyncfunctionsleep(ms:number):Promisevoid{awaitnewPromise((r)setTimeout(r,ms));}/** * 由业务注入本步在「纯 Playwright」路径上要执行什么点击、fill、组合操作等。 * 任意 throw/reject 都会记为当次尝试失败进入重试或后续 AI 分支。 */exporttypePlaywrightAttempt()Promisevoid;/** * 由业务注入可选的收口校验在 PW 成功路径与 Midscene 成功路径上都会执行若传入。 * 用于防止「点到了错误元素但用例仍绿」建议校验 URL、toast、关键 DOM 等。 */exporttypeVerifyAfterAi()Promisevoid;/** * 封装对 Midscene 的调用项目里只维护这一处即可对接版本升级。 * 此处用 aiAct 一次完成「理解 操作」若需拆成 aiTap/aiInput在此函数内改实现。 */exportasyncfunctionrunMidscene(page:Page,goal:string):Promisevoid{// 与当前 Playwright Page 绑定的 Agent由 midscene/web 提供constagentnewPlaywrightAgent(page);awaitagent.aiAct(goal);}/** * 核心编排先循环执行 pwAttempt含可选 verify全部失败后再检查白名单与配额通过则调 Midscene。 */exportasyncfunctionwithPwOrMidsceneFallback(opts:FallbackOptions,pwAttempt:PlaywrightAttempt,verify?:VerifyAfterAi,):PromiseStepResult{const{page,stepId,aiWhitelist,quota,pwRetries,pwRetryDelayMs300,midsceneGoal}opts;// 记录 Playwright 路径上最后一次错误便于在白名单/配额不通过时原样抛出letlastError:Error|undefined;// i 从 0 到 pwRetries共 pwRetries1 次尝试for(leti0;ipwRetries;i){try{awaitpwAttempt();if(verify)awaitverify();return{route:pw,stepId};}catch(e){lastErroreinstanceofError?e:newError(String(e));// 若还有剩余重试次数等待一小段时间再进入下一轮if(ipwRetries)awaitsleep(pwRetryDelayMs);}}// 必须同时满足步骤在白名单内且配额未耗尽constallowAiaiWhitelist.has(stepId)quota.tryConsume();if(!allowAi){throwlastError??newError(Playwright failed and AI not allowed for step:${stepId});}try{awaitrunMidscene(page,midsceneGoal);if(verify)awaitverify();return{route:midscene,stepId};}catch(e){consterreinstanceofError?e:newError(String(e));thrownewError(Midscene fallback failed for [${stepId}]:${err.message},{cause:err});}}在测试用例中的用法playwright/test// Playwright 测试运行器与断言 APIimport{test,expect}fromplaywright/test;// 上节保存的兜底工具模块路径按你项目调整import{MidsceneQuota,withPwOrMidsceneFallback}from./pw-midscene-fallback;/** * 允许走 Midscene 的步骤 id 集合。 * 应由「纯 PW 跑量 flake/失败统计」产出不要随意扩大集合以免浪费 Token。 */constAI_STEPSnewSetstring([login_submit_dynamic_skin,third_party_captcha_row]);test(登录后进入首页,async({page}){// 本用例内 Midscene 最多调用 5 次多步兜底时可共用同一个 quota 实例constquotanewMidsceneQuota(5);// 对「登录提交」这一步启用先 PW含 2 次额外重试仍失败则对白名单步骤调 MidsceneawaitwithPwOrMidsceneFallback({page,// 必须与 AI_STEPS 中某元素一致否则 PW 失败后不会走 AIstepId:login_submit_dynamic_skin,aiWhitelist:AI_STEPS,quota,// 共尝试 123 次 Playwright 路径pwRetries:2,// 自然语言描述与真实界面一致时成功率更高midsceneGoal:在登录弹窗中点击主按钮完成登录不要点注册,},// 第一个回调仅 Playwright 的主路径可把多 locator 链式写在这里async(){awaitpage.getByTestId(login-submit).click({timeout:5000});},// 第二个回调PW 成功或 MS 成功后都会执行用于强断言业务结果async(){awaitexpect(page).toHaveURL(/\/home/);},);// 后续步骤若足够稳定可继续普通 Playwright不必每步都包一层兜底awaitpage.getByRole(link,{name:订单}).click();});可选按「选择器链」尝试再兜底若你希望同一函数内先链式尝试多个 Locator再进 Midscene可把pwAttempt写成// 第一个参数与主示例相同page、stepId、白名单、quota、pwRetries、midsceneGoal 等此处用 /* ... */ 省略awaitwithPwOrMidsceneFallback({/* ... */},// pwAttempt在单次「尝试」内按顺序试多个 Locator任一成功即 return整段失败再抛错以触发重试/AIasync(){// 按优先级排列最稳定、最快的放前面constcandidates[page.getByTestId(submit),page.getByRole(button,{name:/提交|确定/}),page.locator(form).getByRole(button).last(),];letlast:Error|undefined;for(constlocofcandidates){try{awaitloc.click({timeout:4000});return;// 本趟 pwAttempt 成功不再试后续 candidate}catch(e){lasteinstanceofError?e:newError(String(e));}}// 所有候选都失败向外抛错让 withPwOrMidsceneFallback 进入下一轮 PW 重试或 Midscenethrowlast??newError(all locators failed);},// verify与主示例相同由外层传入的校验函数变量名仅作示意verify,);

更多文章