简介这是一份围绕Playwright自动化测试的docx学习文档面向测试工程师、自动化测试初学者及需要开展跨浏览器端到端测试的开发人员帮助读者从环境搭建走到脚本实战与常用方法实践。压缩包内共1个docx文件大小约17.16MB内容以章节化文档形式组织便于按目录检索和系统学习。文档从Playwright概述与多浏览器支持讲起依次覆盖Python Playwright环境要求、PyCharm安装、pip更新、pytest-playwright与内置浏览器下载再通过第一条脚本、百度输入框输入、点击“百度一下”、Web断言、命令行参数、录制用例和base_url使用等实战环节串起脚本编写与调试流程后续还涉及click、提示信息元素定位与断言、go_to导航、hover鼠标悬浮、下拉框操作等常用方法。目前已有389人学习适合希望快速建立Playwright知识框架、对照目录查漏补缺并积累实战思路的读者。1. 从重复的点击回归到一条命令这份 Playwright 自动化测试讲义解决什么接手一个后台系统做回归一轮要手点两百多次改个按钮颜色就得全量重来——这是我第一次认真考虑 Playwright 自动化测试的原因。手上这份 docx 是一套完整课程讲义从环境安装、脚本实战、元素定位一路写到 pytest 分层、PageObject 封装、登录态复用、并发执行与 Allure 报告最后落到一个完整的后台项目实战。它不是 API 速查表而是按「先跑通一条脚本再搭架子」的顺序编排的先让你在十分钟内看到浏览器自己动起来再解释为什么要用 fixture、为什么定位器要分层、为什么失败重试不能乱开。适合两类人写过 Selenium 但被隐式等待和元素定位折磨过的老手以及刚学完 Python、想拿一个真实项目练手的测试新人。下面按这份讲义的骨架把它拆成能照着敲的步骤。2. 环境搭建版本账、三条安装命令与命令行参数装 Playwright 这件事翻车的从来不是 install 本身而是装之前没算清版本账。讲义第一章把要求列得很直白Python 3.8 起步Windows 10 及以上或 WSLmacOS 13/14Linux 侧是 Debian 11/12 与 Ubuntu 20.04/22.04/24.04且同时支持 x86-64 和 arm64。看起来是例行公事实际每条都在后面埋了坑Python 3.8 能装但类型标注写法受限arm64 机器上浏览器内核要下不同的二进制包WSL 里跑无头模式还得补系统依赖。这一章把安装拆成「算账、装包、跑起来」三段每段给一条能直接抄的命令。2.1 装之前先算版本账Python、系统与芯片架构我的习惯是先在目标机器上把三件事确认掉Python 版本、操作系统、处理器架构。前两个决定能不能装第三个决定下载哪个浏览器内核。讲义里 Windows 那段专门提了一句echo %PROCESSOR_ARCHITECTURE%这不是凑字数——AMD64 和 ARM64 拿到的构建产物不一样装错了会在启动浏览器时直接抛错而且报错信息不会告诉你是架构问题。# 一次看清三件事Python 版本、系统平台、CPU 架构 python --version python -c import platform, sys; print(platform.system(), platform.machine(), sys.version_info[:3]) # Windows 上单独确认架构在 cmd 里执行 # echo %PROCESSOR_ARCHITECTURE%这段脚本没有花哨的东西作用是把后面所有「装不上」的排查前置。platform.machine()在 Apple Silicon 上返回arm64在大多数 Windows 机器上返回AMD64或x86_64Linux 服务器上可能是aarch64。看到aarch64就要有心理准备部分旧版本的浏览器内核在 arm 上支持不完整优先把 Python 和 Playwright 都升到较新的小版本。sys.version_info[:3]打出主次修订号比python --version更好解析适合写进 CI 的启动检查脚本里。另一个容易被跳过的是虚拟环境。Playwright 的浏览器内核是装在用户级缓存目录里的跟 Python 环境不完全绑定但pytest-playwright这个包是装在环境里的。我一般给每个项目单独建 venv避免两个项目依赖的 pytest 版本打架——pytest 插件版本冲突导致的报错极难读症状往往是 fixture 找不到实际是插件没被加载。提示Python 3.8 可以跑但如果项目里要用dataclass的新特性或者较新的类型标注写法直接上 3.10 或 3.11省掉后面改语法的返工。2.2 三条命令装完pip、pytest-playwright 与浏览器内核讲义的安装顺序是「更新 pip → 装 pytest-playwright → 下载内置浏览器」这个顺序不能颠倒。pip 版本太旧时解析依赖会拿到过时的 wheel装完 import 就报模块缺失很多新手在这一步以为是网络问题反复重试。把 pip 先升上去能省掉一大半玄学。# 1. 先升 pip用 python -m 调用避免多版本 Python 下 pip 指错解释器 python -m pip install --upgrade pip # 2. 安装 pytest 插件它会顺带把 playwright 主包拉下来 python -m pip install pytest-playwright # 3. 下载三大浏览器内核chromium / firefox / webkit python -m playwright install # Linux 服务器或容器里还要补系统级依赖 python -m playwright install --with-deps第 3 条命令是整段的关键。playwright install不带参数时会把 chromium、firefox、webkit 三套内核全下下来每个几百 MB磁盘吃紧的机器可以按需只装一个比如python -m playwright install chromium。--with-deps只在 Linux 上有意义它会调系统的包管理器补齐字体、图形库等运行时依赖容器镜像里不装这些页面上中文会显示成方块、截图全是空白。参数说明里最值得记的是内核名字它们是固定值chromium、firefox、webkit写错名字命令会直接报错不会静默跳过。如果机器上已经有一份内核缓存换项目时不必重复下载。Playwright 默认把内核放在用户目录下通过环境变量PLAYWRIGHT_BROWSERS_PATH可以指定一个共享目录团队里做 CI 镜像时把这一层烘焙进去构建时间能从几分钟压到十几秒。这个变量在容器里尤其有用镜像层复用时内核不会每次重下。2.3 命令行参数与录制先跑起来再谈架构装完别急着写架构先用命令行参数把第一个脚本跑通看到浏览器窗口自己动起来比读十页文档都管用。讲义里「命令行参数配置」和「录制用例」两节就是干这个的前者让你控制执行形态后者直接生成可用的代码。# 有头模式跑方便看每一步在干什么--slowmo 放慢动作眼睛跟得上 pytest --headed --slowmo500 # 指定浏览器和并发数--browser 可重复传多个值做跨浏览器 pytest --browserchromium --browserfirefox -n 2 # 录制打开浏览器你在页面上点点点代码自动生成 playwright codegen https://example.com--headed关掉无头模式调试阶段必开--slowmo500是每步操作之间插 500 毫秒延迟纯粹给人眼看的别留在正式用例里。-n 2是 pytest-xdist 的并发参数讲义后面专门讲并发时会用到这里先知道有这回事。playwright codegen是录制器边操作边生成代码新手拿它当脚手架最快——但录出来的代码定位器往往又长又脆是「能跑」而不是「能维护」我的用法是先录一遍看它怎么选定位器再按语义定位重写。base_url 也是在这个阶段配的。讲义里单独列了一节原因是硬编码完整 URL 的用例换个环境就要全局替换改漏一处就是线上环境跑测试。配置写进pytest.ini用例里只写相对路径[pytest] base_url https://example.com addopts --tracingretain-on-failure -vbase_url会被page.goto(/login)自动拼接--tracingretain-on-failure表示只在失败时保留 trace 文件这个配置后面排错时会救命——它把每一步操作、网络请求、DOM 快照全录进一个文件用playwright show-trace打开就能像回放录像一样看现场。addopts里的-v只是让终端输出更详细真正有价值的是 trace 那一条。3. 元素定位五种语义定位、CSS/XPath 与 iframe 穿透自动化脚本 80% 的维护成本压在定位器上。讲义把定位单独拆成一章顺序很有意思先讲 Playwright 自带的语义定位再讲 CSS 和 XPath最后讲过滤器与 iframe。这个顺序本身就是在表达态度——优先用语义定位CSS/XPath 是兜底iframe 是特殊情况。语义定位的好处是跟着页面语义走前端换了 class 名、调了 DOM 层级只要按钮还写着「提交」脚本就不会挂。3.1 语义定位五件套role、text、label、placeholder、title这五个方法覆盖了绝大多数交互元素写出来的代码接近人读页面的方式。下面的示例针对一个虚构的搜索页面演示五者的分工。from playwright.sync_api import Page, expect def test_search_flow(page: Page): page.goto(/) # base_url 自动拼接 # 按角色定位按钮、链接、输入框最稳定的一类 search_box page.get_by_role(textbox, name搜索) search_box.fill(Playwright 自动化测试) # 按标签定位表单场景最自然label 文本变了通常是需求变了 page.get_by_label(用户名).fill(demo) # 按占位符定位输入框没有 label 时的退路 page.get_by_placeholder(请输入关键字).fill(pytest) # 按文本定位链接和按钮的兜底注意 use exact 区分模糊匹配 page.get_by_text(登录, exactTrue).click() # 按 title 定位图标按钮常见title 属性就是它的可读名 page.get_by_title(刷新列表).click() expect(page).to_have_url(/search?qPlaywright)逻辑说明每个get_by_*返回的都是一个 locator 对象本身不查 DOM只有在调用fill、click或断言时才真正去定位这跟「先找到元素再操作」的旧思路完全不同好处是元素后加载也不怕Playwright 会自动等待。参数上get_by_role的第一个参数是 ARIA 角色常见值有button、link、textbox、checkbox、combobox第二个参数name匹配无障碍名称通常就是可见文本支持正则get_by_text的exactTrue要显式加上否则「登录」会同时匹配到「登录页」和「退出登录」点到哪个看运气。expect是 Playwright 自带的断言跟 pytest 的assert不是一回事它内置了重试等待断言失败不会立刻判死而是在超时时间内反复重试。这一点新手最容易踩错用普通assert判断文本页面稍微慢一点就红然后加一堆sleep整份脚本跑一次要十几分钟。把断言全部换成expect代码反而更短。3.2 CSS 与 XPath 什么时候还必须上语义定位虽好但现实里总有它够不着的角落动态生成的列表项、没有文本的图标、表格里的行列交叉。这时候才轮到 CSS 和 XPath。两者的取舍我一般是这样判的场景首选理由有稳定 class 或 id 的容器CSS语法短浏览器原生支持执行快需要按文本或父级反向查找XPathCSS 做不了「包含某文本的父节点」表格行列交叉CSS API先用语义定位锁表格再按行列序号取深层嵌套、结构易变都不用让前端加># CSS容器 属性选择器适合结构稳定的列表 rows page.locator(ul.task-list li[data-statuspending]) print(rows.count()) # XPath按文本反向找父级CSS 无能为力的场景 page.locator(xpath//span[text()待处理]/ancestor::div[contains(class,card)]).click() # 约定># 先按文本收窄到一个卡片再在卡片内部找按钮比全局 nth 稳定得多 card page.locator(.task-card).filter(has_text季度报表) card.get_by_role(button, name删除).click() # 已知顺序稳定时才用序号first()/last() 是 nth(0)/nth(-1) 的语法糖 page.get_by_role(row).nth(2).get_by_role(button, name编辑).click() # and / or 组合条件同时满足文本和可见性 page.get_by_role(button).and_(page.get_by_text(导出)).click() # iframe一定要先 frame_locator再在其内部定位 frame page.frame_locator(#editor-frame) frame.get_by_role(textbox).fill(正文内容)filter(has_text...)的参数是模糊匹配has_text还支持正则用re.compile传入更精确filter(has...)则可以传入另一个 locator用于「包含某个子元素」这种结构判断。nth(2)是零基索引第三个才是 2这里翻车过不止一次。and_()和or_()是 Playwright 较新版本才稳定的链式方法条件多的时候比堆 CSS 可读性好。iframe 是另一个高频坑点页面上明明看得到输入框脚本死活报「元素不存在」八成是它在 iframe 里。frame_locator返回的是一个 frame 作用域后续所有定位都在这个作用域内解析。多层嵌套就连续调两次frame_locator不要试图用一个 CSS 选择器穿过去——跨文档边界的选择器在 Playwright 里是不生效的。注意locator是惰性的count()会立刻触发一次查询而后续操作会重新查询。页面结构在操作之间发生变化时别把 locator 当成拿到了元素句柄它更像是「一份查询条件」。4. pytest-playwright 分层四层模型、conftest 与 PageObject 封装脚本能跑通和脚本能维护中间隔着一整套结构。讲义从第六章开始转向设计先把 Playwright 的对象模型对应到 pytest 的 fixture再用 conftest.py 做全局装配最后用 PageObject 把页面细节和用例意图切开。这一段学不学决定了三个月后你还敢不敢动自己的用例。4.1 Browser、Context、Page、Element 四层与 fixture 的对应关系Playwright 的对象是四层的Browser 是进程Context 是隔离的会话独立 cookie、缓存、storagePage 是标签页Element 是页面元素。pytest-playwright 插件把这四层做成了现成的 fixturebrowser、context、page加上browser_name、is_chromium这类判断用的 fixture。理解层级的价值在于知道哪一层该复用、哪一层必须每个用例新建。# 默认 page fixture 是 function 作用域每个用例一个新页面互不干扰 def test_one(page): page.goto(/) # 需要共享登录态时把 scope 提到 session自己建 context import pytest from playwright.sync_api import sync_playwright pytest.fixture(scopesession) def shared_context(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) ctx browser.new_context(viewport{width: 1440, height: 900}) yield ctx ctx.close() browser.close()scopesession意味着整个测试会话只建一次浏览器启动开销从「每用例几百毫秒」降到「整个会话一次」。代价是隔离性变弱上一个用例留下的 cookie 会影响下一个。所以我的做法是 Browser 和 Context 提到 sessionPage 保持在 function——每个用例开新标签页出问题只影响自己。viewport参数指定窗口尺寸涉及响应式布局的断言必须显式设置否则无头模式下的默认尺寸和真实用户看到的完全不是一回事。这一层还有个隐藏收益context隔离让多用户场景变得容易。同一个 Browser 下开两个 Context就是两个互不干扰的「用户」并发登录、协作编辑这类场景不用起两个浏览器进程资源占用低一个量级。4.2 conftest.py 的职责边界conftest.py 是 pytest 的全局装配点讲义里给了它很重的篇幅。我的划分原则是只放「所有用例都需要的装配」和「跨用例的共享资源」不放具体业务逻辑。放进去的典型有三类——浏览器启动参数、登录态准备、失败时的统一钩子。# conftest.py import pytest pytest.fixture(scopesession) def browser_context_args(browser_context_args): # 覆盖插件默认的 context 参数全局生效 return { **browser_context_args, viewport: {width: 1440, height: 900}, ignore_https_errors: True, # 测试环境自签证书常见 locale: zh-CN, } pytest.fixture(autouseTrue) def step_log(request): # 每个用例自动打一条分隔日志定位失败用例时省很多翻找 print(f\n 开始执行: {request.node.name} ) yieldbrowser_context_args是插件预留的钩子 fixture返回的字典会合并进默认参数写法上要先展开原字典再覆盖直接返回新字典会把插件默认值全丢掉。ignore_https_errors在测试环境几乎是必备项自签证书导致的net::ERR_CERT_AUTHORITY_INVALID会让整批用例红掉而它跟被测功能毫无关系。autouseTrue让 fixture 自动作用于所有用例适合做日志、计时这类横向关注点但别拿它做业务数据准备——隐式依赖会让排查变成黑匣子新人接手时根本不知道数据从哪来的。conftest.py 的作用域是按目录继承的。根目录的 conftest 对所有子目录生效子目录可以再放一个 conftest 覆盖父级同名 fixture。这个机制很适合做「模块级」的差异配置比如管理后台和前台门户用不同的登录方式各自目录下放一份同名 fixture 即可用例代码一行不用改。4.3 PageObject把页面元素和用例意图分开PageObject 的核心就一句话用例里只出现业务动作不出现选择器。讲义给的例子是一个搜索页我把它改写成更贴近实际后台的形态# pages/search_page.py from playwright.sync_api import Page, expect class SearchPage: def __init__(self, page: Page): self.page page self.query_input page.get_by_role(textbox, name搜索) self.submit_btn page.get_by_role(button, name搜索) self.result_list page.locator(.result-item) def open(self, base_url: str): self.page.goto(f{base_url}/) return self def search(self, keyword: str): self.query_input.fill(keyword) self.submit_btn.click() return self def expect_results_contain(self, text: str): expect(self.result_list.first).to_contain_text(text)# tests/test_search.py from pages.search_page import SearchPage def test_keyword_search(page, base_url): SearchPage(page).open(base_url).search(Playwright).expect_results_contain(Playwright)逻辑说明页面类在__init__里把 locator 全部声明好方法只做动作组合返回值统一return self以便链式调用。参数上open接收 base_url 而不是硬编码配合 pytest 的base_urlfixture 就能在本地、测试、预发之间切换。测试文件里看不到任何选择器读起来就是一句业务描述前端改版时只需要改pages/下的文件用例文件不动。这个结构真正的价值在协作上。用例由熟悉业务的人写页面类由熟悉前端结构的人维护两边解耦。我见过太多项目把选择器散落在几百个用例文件里一次改版要全局搜索替换改完还得全量回归验证——那基本等于重写。4.4 storage_state 免登录与 pytest-xdist 并发登录是所有后台自动化的第一道坎。每个用例都走一遍登录表单一轮跑下来光登录就耗掉几分钟还容易因为验证码直接卡死。讲义的解法是context.storage_state登录一次把 cookie 和 localStorage 序列化到文件后续 context 直接加载这个文件跳过登录页。# conftest.py import pytest from pathlib import Path from playwright.sync_api import sync_playwright pytest.fixture(scopesession) def auth_file(tmp_path_factory): state_path tmp_path_factory.mktemp(auth) / state.json with sync_playwright() as p: browser p.chromium.launch() ctx browser.new_context() page ctx.new_page() page.goto(https://example.com/login) page.get_by_label(用户名).fill(demo) page.get_by_label(密码).fill(demo-pass) page.get_by_role(button, name登录).click() page.wait_for_url(**/dashboard) # 等跳转完成再存否则会存到未登录状态 ctx.storage_state(pathstr(state_path)) browser.close() return str(state_path) pytest.fixture(scopesession) def browser_context_args(browser_context_args, auth_file): return {**browser_context_args, storage_state: auth_file}这段的关键在wait_for_url。登录接口返回 200 不等于前端已经写入 token如果立刻调storage_state存下来的文件里可能是空的后续所有用例都以未登录状态运行报错却是「找不到某个菜单」排查方向会被带偏很远。path参数指定输出文件文件是 JSON里面能看到 cookies 和 origins 两段出问题时直接打开看有没有关键的会话 cookie比加日志快。并发是下一个坎。pytest-xdist用-n开多进程每个进程都会执行一遍 session 级 fixture如果多个进程同时往同一个state.json写文件就会损坏或者读到半截内容。讲义的解法是加文件锁写之前抢锁写完释放抢不到就等。Linux 上用fcntl.flockWindows 上要用msvcrt.locking跨平台项目直接上filelock这类库更省事。测试数据也是并发下的雷区。多个进程共用一个测试库A 进程删掉的数据可能正是 B 进程要用的。我一般的做法是给每个 worker 分配独立的数据库 schema 或独立的表前缀pytest 的worker_idfixture 能拿到当前进程编号拼进库名里即可。这一步不做用例会呈现典型的「单跑全绿、并发随机红」而且红的位置每次都不一样非常消耗排查时间。4.5 表格与表单的组件化封装后台系统里 70% 的页面是「一个表格加一个表单」把这两类组件封装好剩下的工作就是拼装。表格封装的思路是先锁整表再按行列取交叉元素# components/table.py from playwright.sync_api import Page class Table: def __init__(self, page: Page, root: str .el-table): self.page page self.root page.locator(root) self.headers self.root.locator(thead th) self.rows self.root.locator(tbody tr) def column_index(self, title: str) - int: # 表头文本转列序号避免用例里写死 col3 texts self.headers.all_inner_texts() return [t.strip() for t in texts].index(title) def cell(self, row: int, column: str): idx self.column_index(column) return self.rows.nth(row).locator(td).nth(idx) def row_dict(self, row: int) - dict: # 整行转字典断言时直接比字段报错信息更可读 texts self.rows.nth(row).locator(td).all_inner_texts() headers [h.strip() for h in self.headers.all_inner_texts()] return dict(zip(headers, [t.strip() for t in texts]))column_index是这套封装的核心表头顺序变了、中间插了一列用例不用改因为它按列名找序号而不是写死下标。row_dict把一行转成字典断言时写assert row[状态] 已完成失败信息直接指出哪一列不对比assert texts[4] 已完成强太多。参数root允许传入不同的表格容器选择器同一个类能复用到不同页面。表单封装的难点在类型多样文本框、下拉框、单选框、开关、日期组件每种的操作方式都不一样。讲义的解法是用数据类描述「要填什么」再写一个分发方法按类型自动填from dataclasses import dataclass dataclass class Field: label: str value: str kind: str text # text / select / radio / switch / date def fill_form(page, fields: list[Field]): for f in fields: box page.locator(.form-item).filter(has_textf.label) if f.kind text: box.get_by_role(textbox).fill(f.value) elif f.kind select: box.get_by_role(combobox).click() page.get_by_role(option, namef.value).click() elif f.kind radio: box.get_by_role(radio, namef.value).check() elif f.kind switch: box.get_by_role(switch).set_checked(f.value on) elif f.kind date: box.get_by_role(textbox).fill(f.value) page.keyboard.press(Enter) # 日期组件通常要回车确认Field用dataclass描述好处是可以把一整个表单写成数据列表用例里一眼看清填了什么。kind决定分发路径新增类型只要加一个分支。日期组件那一行press(Enter)是血泪经验很多组件库的下拉面板在失焦后才提交值不回车直接提交表单拿到的还是旧日期。4.6 Allure 报告与失败重试用例跑完总得有人看结果Allure 是目前最省事的选择。安装allure-pytest后用--alluredir指定输出目录再用allure serve起本地服务看报告。讲义里比较有价值的是「多上下文失败截图」那一段并发执行时每个 worker 有独立的 page失败钩子里要能拿到当前用例对应的 page 再截图否则截出来的是别的用例的画面。# conftest.py import pytest, allure pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: page item.funcargs.get(page) if page: allure.attach(page.screenshot(), name失败截图, attachment_typeallure.attachment_type.PNG)hookwrapperTrue让钩子能在测试报告生成前后各插一段逻辑yield之后拿到的report才带结果。item.funcargs里存着当前用例的 fixture 实例从中取page保证截的是自己的画面。这个钩子挂在 conftest 里所有用例自动生效不用在每个用例里写 try/except。失败重试用pytest-rerunfailures--reruns 2表示失败后重跑两次。这个功能要克制使用重试能救的是环境抖动网络超时、服务短暂不可用救不了定位器写错。如果一批用例重试后全绿而单跑第一次总是红说明有问题被掩盖了该去查根因而不是加大重试次数。讲义里还提到「不同轮次用不同 Allure 策略」思路是首次失败只记录不截图重试仍失败才附完整现场避免报告里堆一堆噪音截图。5. 排查避坑五条翻车记录与对应的解决路径这一章全部来自实际踩过的坑每条的形态都很像报错信息指向 A真正的原因在 B。把现象、原因、解决三段拆开看下次遇到类似症状能少绕两小时。5.1 自动等待不是万能的元素可见但点不到现象click()报element is not stable或者点击后没有任何反应日志显示元素已经可见。原因Playwright 的自动等待只保证元素「可操作」——可见、未被禁用、稳定的包围盒。但如果元素被一个透明的遮罩层盖住、或者正在做 CSS 动画位移它仍然算「可见但不可点」。旧版 Selenium 用固定 sleep 掩盖这个问题Playwright 把等待做得更聪明但也让人误以为不用管时序。解决先用expect(locator).to_be_visible()明确等可见再检查是否有遮罩层。动画场景可以用page.wait_for_timeout(300)短暂等待但更好的做法是等动画结束的信号比如某个 class 消失。click(forceTrue)能跳过可操作性检查但这是后悔药不是常规手段用多了会把真实的遮挡 bug 一起跳过。5.2 并发下登录态文件被写坏现象-n 4并发跑随机几个 worker 报未登录重跑又能过。原因session 级 fixture 在每个 worker 进程里都会执行一次四个进程同时写同一个state.json后写的覆盖先写的读到的可能是写了一半的 JSON。解决加文件锁抢到锁的进程负责登录并写文件其余进程等待后直接读。锁的粒度要覆盖「写」的整个过程不能只锁最后一行write。另一个思路是把登录态文件按worker_id分文件存放各写各的代价是登录次数变多。5.3 nth() 写死序号列表排序一变全挂现象昨天还全绿的用例今天第一条就点错了行删掉了不该删的数据。原因列表默认按创建时间倒序新数据一插进来原来的第 3 行变成第 4 行nth(2)指向了别的记录。这类 bug 最危险的地方是它不报错只是操作了错的对象。解决定位行时用业务唯一标识而不是序号page.get_by_role(row).filter(has_text任务名)才是正解。确实需要按序号遍历时先断言这一行的关键字段符合预期再操作让错误尽早暴露。5.4 iframe 里定位失败报错指向根页面现象get_by_role(textbox)报找不到元素但在浏览器里用同样的选择器能选到。原因元素在 iframe 内部根页面的查询根本不会穿透文档边界。报错信息不会提示「你可能在找 iframe 里的东西」只会说元素不存在。解决先page.frame_locator(iframe选择器)拿到作用域再在作用域内定位。调试时可以先page.frames打印所有 frame 的 URL确认目标在哪个 frame 里。富文本编辑器、在线表格、嵌入式地图是 iframe 的三个高发区。5.5 失败重试掩盖了真实缺陷现象CI 上永远全绿本地偶尔红把重试关掉后红一片。原因重试把偶发问题变成了「不阻塞发布」而偶发往往意味着被测系统本身有竞态或者测试脚本时序不稳。两种情况都不该被忽略。解决把重试次数控制在 1 到 2 次同时在 CI 里统计「重试后才通过」的用例数量这个数字持续上升就是明确的预警信号。重试日志要保留不能只看最终结果。注意排查时优先打开 trace 文件它比任何日志都完整。playwright show-trace trace.zip能看到每一步的 DOM 快照元素当时长什么样一目了然。6. 把 trace 变成排错习惯一个具体技巧与验证方法脚本写完不算完能不能在别人接手后快速定位失败取决于你有没有留下足够的现场。我现在的固定动作是所有 CI 任务默认带--tracingretain-on-failure配合--output指定产物目录失败用例的 trace 直接归档成构建产物。看 trace 的时候不需要重新跑一遍时间轴拖到失败那一步左边是操作列表中间是 DOM 快照右边是网络请求和控制台输出三栏一对问题基本藏不住。一个具体技巧是给关键步骤加page.pause()。这个调用只在有头模式下生效会在那一步暂停并打开调试器可以现场改选择器、看元素属性。开发期我在写完一段新逻辑后必然插一个pause()走一遍确认定位器在真实 DOM 上稳定再把它删掉。比反复改代码重跑快得多。验证脚本稳定性我一般跑三轮单进程跑一遍看功能对不对四进程并发跑一遍看隔离性再把网络限速到 3G 跑一遍看等待逻辑够不够。第三轮最容易出问题很多expect的超时默认值在慢网络下不够用需要针对具体断言调timeout参数而不是全局调大——全局调大只会让失败来得更晚。验证轮次目的观察指标单进程全量功能正确性通过率、断言失败信息四进程并发数据与登录态隔离随机失败率、文件冲突日志限速慢网等待逻辑健壮性超时次数、重试后通过数从那以后我每次交付一套用例都强制走一遍这三轮尤其是并发那轮——它揪出来的问题往往是脚本写的时候完全想不到的。希望帮到你。本文还有配套的精品资源点击获取