ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Rocket.Chat Playwright 测试怎么写定位器:role/text/page object 规范与常见反模式

Rocket.Chat Playwright 测试怎么写定位器:role/text/page object 规范与常见反模式 Rocket.Chat Playwright 测试怎么写定位器role/text/page object 规范与常见反模式【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat需要在 Rocket.Chat 仓库里写一个新的 e2e 测试或改动现有定位器又不想让它过几天就碎掉要遵循的规范都写在 apps/meteor/tests/e2e/README.md 里定位器优先用 role 定位、写在 page object 里并限制作用域、避免 testId / 类名 / 位置类定位器。下面按“启动测试应用 → 选定位器 → 写 page object → 运行验证”的顺序走一遍所有规则都以该 README 和仓库内的真实测试代码为准。准备启动测试应用并跑通单个用例Playwright 测试必须跑在一个已启动的实例上应用需要带TEST_MODEtrue启动TEST_MODEtrue yarn dev然后在apps/meteor下运行单个 suiteyarn test:e2e ./tests/e2e/administration.spec.ts也可以直接yarn test:e2e跑全部测试。test:e2e支持两个环境变量BASE_URLany_url把测试指向指定 URLPWDEBUG1控制调试测试执行过程。写新测试时注意一个细节本仓库的用例不是直接从playwright/test导入test/expect而是从 utils/test.ts 导入。它在 Playwright 的 test 基础上扩展了 fixture包括api带X-Auth-Token/X-User-Id请求头的 REST 上下文用于准备测试数据和makeAxeBuilder基于 axe-core 的无障碍检查。所以新测试文件的第一行导入是import { test, expect } from ./utils/test;一个完整的真实用例参考 sidebar-custom-categories.spec.ts。定位器怎么选role 优先其次 text/label最后是 has1. 尽量用.getByRole()README 明确说 role 是“最推荐的定位器类型”理由有三条保证元素对屏幕阅读器等辅助技术可访问保证元素可被唯一标识建议配合exact选项避免定位器误匹配同 role 的其他元素。await page.getByRole(button, { name: Save, exact: true }); // Without the exact option, Playwright would match buttons with Save and Save changes labels.2. role 不够时再用.getByText()/.getByLabel()当按 role 无法定位、或 role 不够精确时使用 text 或 label。前提是“把定位器的作用域限制到你真正想找的那个元素”——即先定位到容器某个 role 或某个 page object 属性再在容器内部找具体元素而不是在整页范围裸跑一个文本查找。3. 自定义 input 用has定位Rocket.Chat 的 input 元素会隐藏原生 input、渲染自定义组件。README 给出的做法定位包裹 input 的 label然后用has定位器page.locator(label, { has: this.page.getByRole(checkbox, { name: Private }) });必须避免的定位器类型README 把下面四类列为 “Locator types to avoid (at all costs)”1.data-qa-id/ testId.getByTestId()// DONT page.locator([data-qa-idmenu-more-actions]); // DO page.getByRole(menu, { name: More actions, exact: true });2. 基于元素和类名的选择器.locator(div.class-name)HTML 结构和类名都会随时间变化。README 的反例是一条很长的类名链#modal-root .rcx-button-group--align-end .rcx-button--primary它建议 modal 用 roledialog name 定位再在 dialog 内部找按钮page.getByRole(dialog, { name: Modal name example }).getByRole(button, { name: Confirm, exact: true });3. 基于位置的选择器nth-child之类容易变化脆弱、难维护。4. 父子关系选择器DOM 结构一变测试就直接挂掉。README 还给了一个判断标准如果你无法用 role、label 或 text 定位某个元素这本身就是信号——说明该元素对不可访问的用户而言也“不存在”。正确做法是重构组件让它可以被可访问的定位器找到而不是补一个 testId 绕过。把定位器写进 page object命名规范定位器名称必须以这些前缀之一开头btn、link、input、select、checkbox、text。仓库里的 home-channel.ts 遵循这一约定例如btnJoinChannel、btnContextualbarClose、btnRoomSaveE2EEPassword。先找现成的 page object 再新建page object 目录是apps/meteor/tests/e2e/page-objects通过getters和methods在多个测试间复用定位器。README 的要求写新测试前先查看现有 page object 是否有适合当前场景的没有的话评估是否值得创建可复用的 getter/method并在“相应的上下文”里创建而不是散落在 spec 文件里。以 sidebar 行为为例先看 page-objects/fragments/sidebar.ts 里是否已有需要的 fragment没有就在这个文件里新增 getter 或方法。该文件的真实写法export class RoomSidebar extends Sidebar { constructor(protected page: Page) { super(page.getByRole(navigation, { name: Sidebar })); // ... } get btnClose(): Locator { return this.root.getByRole(button, { name: Close }); } }基类Sidebar的构造函数接收一个root: Locator各子类把 root 锚定到不同导航区Sidebar/Administration/Account/Omnichannelgetter 一律基于this.root派生——这就是“限制作用域”的具体落地。作用域不要拿整页定位README 的要求是 “Always make sure to use the most restricted scope possible - not the whole page to avoid multiple matches”。即 getter 应从尽量窄的容器开始而不是从this.page开始。README 给出的层级示例get channelsList(): Locator { return this.sidebar.getByRole(list, { name: Channels }); } // Restricted scope: inside navigation sidebar list named Channels link with name getSearchRoomByName(name: string) { return this.channelsList.getByRole(link, { name }); }测试里的用法就变成一行README 中的示例test(should display sidebar items, async ({ page }) { poHomeChannel new HomeChannel(page); await page.goto(/home); const targetChannel channel-test; await expect(poHomeChannel.sidebar.getSearchRoomByName(targetChannel)).toBeVisible(); });home-channel.ts 里还有一个把“导航 等待就绪”封装成方法的可运行例子async gotoChannel(name: string) { await this.page.goto(/channel/${name}); await this.content.waitForChannel(); }当测试已经知道自己要进哪个房间时直接调poHomeChannel.gotoChannel(name)比page.goto(/home)后在 navbar 搜索更稳搜索路径要等搜索索引同步任一环节卡住都会让fill挂到超时。断言与等待把“完成”写成可观察条件README 对断言和等待的规则优先使用 Playwright 的 web-first 断言如toBeVisible()、toHaveText()它们会重试直到条件满足用 Playwright 的expectmatchers而不是 Node 风格的assert尽可能依赖 Playwright 内置的自动等待必须显式等待时等一个具体条件locator.waitFor()、page.waitForURL()、page.waitForResponse()优先等可观察条件避免固定延时如果固定延时不可避免要在代码里写明为什么没有可等待的可观察条件。await expect(anyElement).toBeVisible();运行、验证与调试# 运行单个用例 yarn test:e2e ./tests/e2e/administration.spec.ts # 运行全部用例 yarn test:e2e不改代码验证行为时加PWDEBUG1运行在调试面板里查看元素的 role 和 name对照本文的判断标准这个元素能不能用 role / label / text 定位需要把测试指向另一台实例时用BASE_URLany_url传给test:e2e。测试红了先检查自己的定位器是否落在“必须避免”的四类里testId、类名、位置、父子链。按 README 的判断这类定位器会随着 DOM 变化持续碎掉正确动作是回到组件侧让它能用 role 定位而不是改测试。边界与下一步新测试要用test.afterAll()/test.afterEach()清理删除测试期间创建的用户、频道、房间等把改动过的设置恢复默认值关闭测试中打开的新页面。README 末尾给出了含setSettingValueById、deleteChannel等 helper 的清理示例。README 的 “Anti-patterns to flag in review” 一节列的是 setup 与导航层面的红线例如beforeEach里用 UI 发消息、test.describe.serial配beforeEach里重复page.goto、用 navbar 搜索去访问已知房间与本文的定位器规则在 review 中一起检查详见 apps/meteor/tests/e2e/README.md。【免费下载链接】Rocket.ChatThe Secure CommsOS™ for mission-critical operations项目地址: https://gitcode.com/GitHub_Trending/ro/Rocket.Chat创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进