You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

禁用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选择器,需实现以下测试需求:

  1. 验证菜单结构(显示文本、顺序及嵌套关系)
  2. 通过点击菜单项完成导航操作

一、验证菜单结构的测试实现

核心思路是利用页面的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 18:03:24