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_MODE=true启动:
TEST_MODE=true yarn dev然后在apps/meteor下运行单个 suite:
yarn test:e2e ./tests/e2e/administration.spec.ts也可以直接yarn test:e2e跑全部测试。test:e2e支持两个环境变量:
BASE_URL=<any_url>:把测试指向指定 URL;PWDEBUG=1:控制(调试)测试执行过程。
写新测试时注意一个细节:本仓库的用例不是直接从@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,最后是 has
1. 尽量用.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())
// DON'T page.locator('[data-qa-id="menu-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 用 role'dialog'+ 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/Omnichannel),getter 一律基于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- 不改代码验证行为时,加
PWDEBUG=1运行,在调试面板里查看元素的 role 和 name,对照本文的判断标准:这个元素能不能用 role / label / text 定位; - 需要把测试指向另一台实例时,用
BASE_URL=<any_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),仅供参考