import { expect, test } from '@playwright/test' /** * 双列菜单布局(layout = 'sidebar-mixed-nav')渲染回归 —— 针对 ERP 应用。 * * 组件编过 ≠ 页面渲染对,所以这里在真实浏览器里断言 DOM 结构: * 1. 切到双列布局后,左侧图标栏 + 第二列都存在; * 2. 单列侧栏(.layout-sidebar)不再渲染; * 3. 第一列只显示顶级项(图标 + 标题),不含子项; * 4. 第二列显示当前顶级分组的子项; * 5. 点第一列另一个分组,第二列内容随之切换。 * * 说明:放在组件库 e2e 工程里跑(已有 Playwright 配置), * 通过 ERP_URL 指向 ERP dev server,避免给 ERP 应用引入 Playwright 依赖。 */ const ERP_URL = process.env.ERP_URL ?? 'http://localhost:5090' /** 通过偏好设置把布局切成双列菜单(直接写 localStorage,避免点 UI 的耦合) */ async function useMixedNavLayout(page: import('@playwright/test').Page) { await page.goto(`${ERP_URL}/`) await page.evaluate(() => { const KEY = 'g3erp:preferences' const raw = localStorage.getItem(KEY) const pref: Record> = raw ? JSON.parse(raw) : {} const app = pref.app || {} app.layout = 'sidebar-mixed-nav' pref.app = app const sidebar = pref.sidebar || {} sidebar.enable = true pref.sidebar = sidebar localStorage.setItem(KEY, JSON.stringify(pref)) // 清掉页签持久化,保证每次从干净状态开始 for (const key of Object.keys(localStorage)) { if (key.indexOf('g3erp:app') === 0) localStorage.removeItem(key) } }) await page.reload() await page.waitForTimeout(400) } test.describe('ERP 双列菜单布局', () => { test('渲染出图标栏 + 第二列,且不再渲染单列侧栏', async ({ page }) => { await useMixedNavLayout(page) // 布局根节点带上了对应 class await expect(page.locator('.layout--sidebar-mixed-nav')).toHaveCount(1) // 两列都在 await expect(page.locator('.layout-mixed-rail')).toBeVisible() await expect(page.locator('.layout-mixed-column')).toBeVisible() // 单列侧栏不该出现 await expect(page.locator('.layout-sidebar')).toHaveCount(0) }) test('第一列只放顶级项,第二列放该分组的子项', async ({ page }) => { await useMixedNavLayout(page) // 进入海运下的某个叶子,使「海运」成为当前顶级分组 await page.goto(`${ERP_URL}/ocean/booking`) await page.waitForTimeout(400) const railItems = page.locator('.layout-mixed-rail__item') await expect(railItems.first()).toBeVisible() // 第一列项数应等于顶级菜单数(6 个业务域) const railCount = await railItems.count() expect(railCount, '第一列应为顶级菜单数量').toBeGreaterThanOrEqual(5) // 第一列里不应出现二级标题(如「订舱单」) const railText = (await railItems.allTextContents()).join('|') expect(railText, '第一列不应包含子项「订舱单」').not.toContain('订舱单') expect(railText, '第一列应含顶级项「海运」').toContain('海运') // 第一列里「海运」应是选中态 const activeRail = page.locator('.layout-mixed-rail__item.is-active') await expect(activeRail).toHaveCount(1) await expect(activeRail).toContainText('海运') // 第二列显示「海运」的子项 const columnText = await page.locator('.layout-mixed-column').innerText() expect(columnText, '第二列应含「海运」的子项').toContain('订舱单') }) test('点第一列切换到别的分组,第二列内容随之变化', async ({ page }) => { await useMixedNavLayout(page) await page.goto(`${ERP_URL}/ocean/booking`) await page.waitForTimeout(400) const columnBefore = await page.locator('.layout-mixed-column').innerText() expect(columnBefore).toContain('订舱单') // 点第一列的「财务」 await page.locator('.layout-mixed-rail__item', { hasText: '财务' }).click() await page.waitForTimeout(500) const columnAfter = await page.locator('.layout-mixed-column').innerText() expect(columnAfter, '切到财务后第二列应显示财务的子项').toContain('应收费用') expect(columnAfter, '第二列不应再显示海运的子项').not.toContain('订舱单') // 「财务」应变成选中态 await expect(page.locator('.layout-mixed-rail__item.is-active')).toContainText('财务') }) })