禁用XPath/CSS选择器时,如何编写菜单/子菜单测试?
问题描述
给定如下导航栏HTML结构:
<ul class="k-widget k-reset k-header k-menu k-menu-horizontal" id="Menu" data-role="menu" tabindex="0" role="menubar"> <li class="k-item k-state-highlight k-state-default k-first" role="menuitem"><a class="k-link" href="/Home/">Home</a></li> <li class="k-item k-state-default" role="menuitem"><span class="k-link">Configurations<span class="k-icon k-i-arrow-s"></span></span> <ul class="k-group" role="menu" aria-hidden="true"> <li class="k-item k-state-default k-first" role="menuitem"><span class="k-link">Config for A<span class="k-icon k-i-arrow-e"></span></span> <ul class="k-group" role="menu" aria-hidden="true"> <li class="k-item k-state-default k-first" role="menuitem"><a class="k-link" href="url1">SubConfig A</a></li> <li class="k-item k-state-default" role="menuitem"><a class="k-link" href="url">SubConfig B</a></li> <li class="k-item k-state-default k-last" role="menuitem"><a class="k-link" href="url2">SubConfig C</a></li> </ul> </li> <li class="k-item k-state-default" role="menuitem"><span class="k-link">Config for B<span class="k-icon k-i-arrow-e"></span></span> <ul class="k-group" role="menu" aria-hidden="true"> <li class="k-item k-state-default k-first" role="menuitem"><a class="k-link" href="url3">SubConfig A</a></li> <li class="k-item k-state-default" role="menuitem"><a class="k-link" href="url4">SubConfig B</a></li> <li class="k-item k-state-default k-last" role="menuitem"><a class="k-link" href="url5">SubConfig C</a></li> </ul> </li> <li class="k-item k-state-default" role="menuitem"><span class="k-link">Config for C<span class="k-icon k-i-arrow-e"></span></span> <ul class="k-group" role="menu" aria-hidden="true"> <li class="k-item k-state-default k-first" role="menuitem"><span class="k-link">Config for C-1<span class="k-icon k-i-arrow-e"></span></span> <ul class="k-group" role="menu" aria-hidden="true"> <li class="k-item k-state-default k-first" role="menuitem"><a class="k-link" href="url6">SubConfig A</a></li> <li class="k-item k-state-default" role="menuitem"><a class="k-link" href="url7">SubConfig B</a></li> <li class="k-item k-state-default k-last" role="menuitem"><a class="k-link" href="url8">SubConfig C</a></li> </ul> </li> <li class="k-item k-state-default" role="menuitem"><span class="k-link">Config for C-2<span class="k-icon k-i-arrow-e"></span></span> <ul class="k-group" role="menu" aria-hidden="true"> <li class="k-item k-state-default k-first" role="menuitem"><a class="k-link" href="url9">SubConfig A</a></li> <li class="k-item k-state-default" role="menuitem"><a class="k-link" href="url10">SubConfig B</a></li> <li class="k-item k-state-default k-last" role="menuitem"><a class="k-link" href="url11">SubConfig C</a></li> </ul> </li> <li class="k-item k-state-default k-last" role="menuitem"><a class="k-link" href="url12">Global</a></li> </ul> </li> </ul> </li> </ul>
文档建议禁止使用XPath或CSS选择器,需实现以下测试需求:
- 验证菜单结构(显示文本、顺序及嵌套关系)
- 通过点击菜单项完成导航操作
一、验证菜单结构的测试实现
核心思路是利用页面的ARIA角色属性和语义化结构定位元素,完全避开CSS/XPath选择器。以下是具体实现步骤:
1. 定义预期菜单结构数据
先整理出预期的菜单层级、文本、顺序,形成结构化数据,方便后续断言:
const expectedMenuStructure = [ { text: "Home", isLink: true, children: [] }, { text: "Configurations", isLink: false, children: [ { text: "Config for A", isLink: false, children: [ { text: "SubConfig A", isLink: true }, { text: "SubConfig B", isLink: true }, { text: "SubConfig C", isLink: true } ] }, { text: "Config for B", isLink: false, children: [ { text: "SubConfig A", isLink: true }, { text: "SubConfig B", isLink: true }, { text: "SubConfig C", isLink: true } ] }, { text: "Config for C", isLink: false, children: [ { text: "Config for C-1", isLink: false, children: [ { text: "SubConfig A", isLink: true }, { text: "SubConfig B", isLink: true }, { text: "SubConfig C", isLink: true } ] }, { text: "Config for C-2", isLink: false, children: [ { text: "SubConfig A", isLink: true }, { text: "SubConfig B", isLink: true }, { text: "SubConfig C", isLink: true } ] }, { text: "Global", isLink: true } ] } ] } ];
2. 递归遍历验证菜单
通过role="menubar"定位顶级菜单容器,递归遍历每个role="menuitem"元素,与预期数据对比:
- 顶级菜单:找到
role="menubar"元素,获取其下所有role="menuitem"子元素 - 子菜单:若菜单项包含
role="menu"子容器,说明是带下拉的父菜单,继续递归遍历内部role="menuitem" - 断言点:菜单项的显示文本、顺序、嵌套层级需与预期匹配;区分链接型菜单项和父菜单
示例代码(Playwright):
async function validateMenuLevel(menuContainer, expectedItems) { const menuItems = await menuContainer.locator('[role="menuitem"]').all(); // 验证菜单项数量 expect(menuItems.length).toBe(expectedItems.length); for (let i = 0; i < menuItems.length; i++) { const item = menuItems[i]; const expected = expectedItems[i]; // 清理文本(去除箭头图标) const itemText = await item.textContent(); const cleanText = itemText.replace(/\s*↓|\s*→/g, "").trim(); expect(cleanText).toBe(expected.text); // 验证是否为链接型菜单项 const hasLink = await item.locator('a').count() > 0; expect(hasLink).toBe(expected.isLink); // 递归验证子菜单 if (expected.children.length > 0) { await item.hover(); // 触发悬停显示子菜单,依组件逻辑替换为click() const subMenu = item.locator('[role="menu"]'); await subMenu.waitFor({ state: "visible" }); await validateMenuLevel(subMenu, expected.children); } } } // 执行测试 test("验证菜单结构正确性", async ({ page }) => { await page.goto("/"); const topMenu = page.locator('[role="menubar"]'); await validateMenuLevel(topMenu, expectedMenuStructure); });
二、点击菜单项导航的测试实现
同样基于ARIA角色和文本定位,无需CSS/XPath:
1. 直接导航的菜单项
对于可直接跳转的菜单项(如Home),通过角色+文本组合定位,点击后验证跳转:
test("点击Home菜单项导航", async ({ page }) => { await page.goto("/"); // 定位role为menuitem且文本为Home的元素,点击内部链接 await page.locator('[role="menuitem"]', { hasText: "Home" }).locator('a').click(); // 验证跳转结果 await expect(page).toHaveURL("/Home/"); await expect(page.locator("h1")).toHaveText("Home Page"); });
2. 多级菜单的导航操作
对于嵌套菜单项,需先展开父菜单,再定位子菜单项点击:
test("点击Config for C-1下的SubConfig B导航", async ({ page }) => { await page.goto("/"); // 展开Configurations菜单 const configParent = page.locator('[role="menuitem"]', { hasText: "Configurations" }); await configParent.hover(); await configParent.locator('[role="menu"]').waitFor({ state: "visible" }); // 展开Config for C菜单 const configCParent = configParent.locator('[role="menu"]').locator('[role="menuitem"]', { hasText: "Config for C" }); await configCParent.hover(); await configCParent.locator('[role="menu"]').waitFor({ state: "visible" }); // 展开Config for C-1菜单 const configC1Parent = configCParent.locator('[role="menu"]').locator('[role="menuitem"]', { hasText: "Config for C-1" }); await configC1Parent.hover(); await configC1Parent.locator('[role="menu"]').waitFor({ state: "visible" }); // 点击SubConfig B并验证跳转 await configC1Parent.locator('[role="menu"]').locator('[role="menuitem"]', { hasText: "SubConfig B" }).locator('a').click(); await expect(page).toHaveURL("url7"); });
关键注意点
- 若菜单展开逻辑为点击而非悬停,将
hover()替换为click()即可 - 所有定位依赖ARIA角色和菜单项文本,避开了易变的CSS类名、ID,测试稳定性更高
内容的提问来源于stack exchange,提问作者Nafas
相关产品推荐
相关产品推荐

