import { expect, test } from '@playwright/test' /** * 双列菜单布局的头部区域回归 —— 针对 ERP 应用。 * * 结构对照 vben(packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue): * 第一列(layout-mixed-rail,width = mixedWidth): * · 背景 bg-sidebar-deep(本项目按需求用白色) * · 在这一列(:410,高度 = headerHeight) * 第二列(layout-mixed-column,extraWidth): * · 背景 bg-sidebar * · 在这一列(:446,高度 = extraTitleHeight ?? headerHeight - 1) * * ⚠️ 注意区分:vben 的 `isMixedNav` = layout === 'mixed-nav'(顶栏混合), * 与本布局 `sidebar-mixed-nav` 不是一回事。vben-layout.vue:619 的 * `isSideMode && !isMixedNav && sidebarLogoVisible` 对 sidebar-mixed-nav 成立, * 所以 logo 渲染在第一列。 * * 即:logo 在第一列,标题在第二列。 */ const ERP_URL = process.env.ERP_URL ?? 'http://localhost:5090' 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 sidebar.extraCollapse = false pref.sidebar = sidebar const logo = pref.logo || {} logo.enable = true logo.showText = true pref.logo = logo 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('logo 在第一列(layout-mixed-rail)', async ({ page }) => { await useMixedNavLayout(page) await page.goto(`${ERP_URL}/ocean/booking`) await page.waitForTimeout(400) const rail = page.locator('.layout-mixed-rail') await expect(rail).toBeVisible() // 第一列顶部有 logo 方块 const mark = rail.locator('.layout-mixed-rail__logo-mark') await expect(mark, '第一列应有 logo 方块').toBeVisible() await expect(mark).toHaveText('G3') // 有实际尺寸,不是被压成 0 const box = await mark.boundingBox() expect(box, 'logo 方块应有实际尺寸').not.toBeNull() expect(box!.width).toBeGreaterThan(10) expect(box!.height).toBeGreaterThan(10) // 第二列不应再出现 logo await expect(page.locator('.layout-mixed-column__logo')).toHaveCount(0) // 顶栏也不应出现 logo(双列布局下由第一列承载) await expect(page.locator('.layout-header__logo')).toHaveCount(0) }) test('标题行展示的是 logo 文字(不是分组名)', async ({ page }) => { await useMixedNavLayout(page) await page.goto(`${ERP_URL}/ocean/booking`) await page.waitForTimeout(400) const title = page.locator('.layout-mixed-column__title') await expect(title, '第二列标题行应可见').toBeVisible() // 应是 logo 文字(默认 G3Soft ERP),而不是当前分组名「海运」 const text = (await title.innerText()).trim() expect(text, '标题行应是 logo 文字').not.toBe('海运') expect(text.length, '标题行应有内容').toBeGreaterThan(0) // 与单列侧栏/顶栏的 logo 文字取自同一偏好项 const logoText = await page.evaluate(() => { const raw = localStorage.getItem('g3erp:preferences') const pref = raw ? JSON.parse(raw) : {} return pref.logo ? pref.logo.text : null }) expect(logoText, '应能读到 logo.text 偏好').toBeTruthy() expect(text, '标题行文本应等于 logo.text').toBe(logoText) const box = await title.boundingBox() expect(box).not.toBeNull() expect(box!.height).toBeGreaterThan(20) }) test('切换分组不影响标题行(它显示 logo 文字,与分组无关)', async ({ page }) => { await useMixedNavLayout(page) await page.goto(`${ERP_URL}/ocean/booking`) await page.waitForTimeout(400) const before = (await page.locator('.layout-mixed-column__title').innerText()).trim() await page.locator('.layout-mixed-rail__item', { hasText: '财务' }).click() await page.waitForTimeout(500) const after = (await page.locator('.layout-mixed-column__title').innerText()).trim() expect(after, '标题行是 logo 文字,切分组不应变化').toBe(before) // 但列表内容确实换成了财务的子项 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 rail = page.locator('.layout-mixed-rail') const bg = await rail.evaluate((el) => getComputedStyle(el).backgroundColor) expect(bg, `图标栏背景应为白色,实际 ${bg}`).toMatch(/^rgba?\(255,\s*255,\s*255(,\s*1)?\)$/) }) })