Files
workspace/code/g3soft-libs/packages/ui/e2e/ui-regressions.spec.ts
T
2026-10-08 22:38:34 +08:00

225 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from '@playwright/test'
/**
* 组件细节回归 —— 每一条都对应一个真实报障,之前全部漏测。
*
* 共同点:它们要么靠布局计算(滑块尺寸、等高、线性位移),要么靠伪类层叠
* (:hover 的特异性、相邻边框的 z-index),要么靠 CSS 变量的实际解析结果
* (color-mix 里的 var())。这些在 happy-dom 里量不出来,必须放 E2E。
*/
test.describe('ButtonGroup', () => {
test('横向:hover 的按钮浮到上层,被压住的那条边框仍可见', async ({ page }) => {
await page.goto('/')
const second = page.getByTestId('btn-group-plain').locator('.g3-button').nth(1)
await second.hover()
// 相邻按钮用 -1px 负边距重叠,不抬 z-index 时后一个按钮的边框会盖住它
await expect(second).toHaveCSS('z-index', '1')
// 边框色有 0.2s 过渡:必须用会重试的断言,直接 evaluate 会读到过渡中间值
await expect(second).toHaveCSS('border-top-color', 'rgb(24, 24, 27)')
})
test('纵向:hover 的按钮同样抬升', async ({ page }) => {
await page.goto('/')
const second = page.getByTestId('btn-group-vertical').locator('.g3-button').nth(1)
await second.hover()
await expect(second).toHaveCSS('z-index', '1')
await expect(second).toHaveCSS('border-top-color', 'rgb(24, 24, 27)')
})
})
test.describe('Segmented', () => {
test('选中滑块与选中项同尺寸同位置(滑块曾塌成 0 高,看不出选中)', async ({ page }) => {
await page.goto('/')
// 必须限定在同一个实例内:页面上有多组 Segmented,跨实例比较会拿到别人的滑块
const group = page.locator('.g3-segmented__group').first()
const thumb = group.locator('.g3-segmented__thumb')
const item = group.locator('.g3-segmented__item.is-selected')
const t = (await thumb.boundingBox())!
const i = (await item.boundingBox())!
// 滑块尺寸来自 offsetWidth/offsetHeight(整数),实际布局是亚像素,允许 1px 误差
for (const [name, a, b] of [
['width', t.width, i.width],
['height', t.height, i.height],
['x', t.x, i.x],
['y', t.y, i.y],
] as const) {
expect(Math.abs(a - b), `滑块与选中项的 ${name} 不一致`).toBeLessThanOrEqual(1)
}
expect(t.height).toBeGreaterThan(0)
// 滑块就是选中底色,不能是透明的
expect(await thumb.evaluate((el) => getComputedStyle(el).backgroundColor)).toBe(
'rgb(255, 255, 255)',
)
})
})
test.describe('Pagination', () => {
test('hover 当前页仍保持主色填充(曾被通用 hover 覆盖成白底白字)', async ({ page }) => {
await page.goto('/')
const current = page.locator('.g3-pagination__page.is-page-current')
await current.hover()
await expect(current).toHaveCSS('background-color', 'rgb(24, 24, 27)')
await expect(current).toHaveCSS('color', 'rgb(250, 250, 250)')
})
})
test.describe('Tooltip', () => {
test('气泡没有描边(投影由 popup 的 drop-shadow 统一施加)', async ({ page }) => {
await page.goto('/')
await page.getByTestId('tooltip-trigger').hover()
const content = page.locator('.g3-popover__content').first()
await expect(content).toBeVisible()
await expect(content).toHaveCSS('border-top-width', '0px')
await expect(content).toHaveCSS('border-left-width', '0px')
})
})
test.describe('Tabs 垂直方向', () => {
for (const placement of ['left', 'right'] as const) {
test(`${placement}:标签栏与内容区之间有竖向分隔线,选中项带侧边指示线与底色`, async ({
page,
}) => {
await page.goto('/')
const tabs = page.locator(`.g3-tabs--${placement}`).first()
const nav = tabs.locator('.g3-tabs__nav')
const active = tabs.locator('.g3-tabs__tab.is-tab-active').first()
if (placement === 'left') {
await expect(nav).toHaveCSS('border-right-width', '1px')
await expect(active).toHaveCSS('border-right-width', '2px')
} else {
await expect(nav).toHaveCSS('border-left-width', '1px')
await expect(active).toHaveCSS('border-left-width', '2px')
}
// 选中项底色不再是 transparent
const bg = await active.evaluate((el) => getComputedStyle(el).backgroundColor)
expect(bg).not.toBe('rgba(0, 0, 0, 0)')
})
}
})
test.describe('Tree', () => {
test('点击节点有选中底色(token color-mix 曾解析失败导致背景透明)', async ({ page }) => {
await page.goto('/')
const node = page.locator('.g3-tree-node', { hasText: '节点一' }).first()
await node.click()
await expect(node).toHaveClass(/is-selected/)
const bg = await node.evaluate((el) => getComputedStyle(el).backgroundColor)
expect(bg, `选中底色不应为透明:${bg}`).not.toBe('rgba(0, 0, 0, 0)')
})
test('整层都是叶子时不渲染展开箭头占位', async ({ page }) => {
await page.goto('/')
// 扁平树:两个节点都没有子节点
const flatTree = page.locator('.g3-tree').filter({ hasText: '节点一(无子节点)' }).first()
await expect(flatTree.locator('.g3-tree-node')).toHaveCount(2)
await expect(flatTree.locator('.g3-tree-node__switcher')).toHaveCount(0)
})
test('拖拽需要长按武装:直接拖动不会产生落点', async ({ page }) => {
await page.goto('/')
const draggableTree = page.locator('.g3-tree').filter({ hasText: '节点一(无子节点)' })
// 用带 draggable 的那棵树里的第一行
const row = page.locator('.g3-tree-node').first()
const box = (await row.boundingBox())!
await page.mouse.move(box.x + 40, box.y + box.height / 2)
await page.mouse.down()
// 立刻移动(未到 400ms 阈值):draggable 属性还没武装
await page.mouse.move(box.x + 60, box.y + box.height / 2 + 30, { steps: 3 })
expect(await row.getAttribute('draggable')).toBeNull()
await page.mouse.up()
expect(await draggableTree.count()).toBeGreaterThan(0)
})
})
test.describe('DatePicker 时间列', () => {
test('时/分/秒三列并排:定宽、列间分隔线、列表与日历等高且可滚动', async ({ page }) => {
await page.goto('/')
await page.locator('.g3-date-picker__input').first().click()
const calendar = page.locator('.g3-calendar')
await expect(calendar).toBeVisible()
const cols = calendar.locator('.g3-time-select__col')
await expect(cols).toHaveCount(3)
await expect(cols.first()).toHaveCSS('width', '56px')
await expect(cols.nth(1)).toHaveCSS('border-left-width', '1px')
const main = (await calendar.locator('.g3-calendar__main').boundingBox())!
const time = (await calendar.locator('.g3-calendar__time').boundingBox())!
expect(Math.round(main.height)).toBe(Math.round(time.height))
const list = calendar.locator('.g3-time-select__list').first()
const listBox = (await list.boundingBox())!
expect(Math.round(listBox.height)).toBe(Math.round(main.height))
expect(
await list.evaluate((el) => el.scrollHeight > el.clientHeight),
'时间列应当内部滚动而不是撑开面板',
).toBe(true)
// 单个选项 28px 高
await expect(calendar.locator('.g3-time-select__item').first()).toHaveCSS('height', '28px')
})
test('年 / 月选择面板:3 列 × 4 行,标题显示年份区间,当前年有标记', async ({ page }) => {
await page.goto('/')
await page.locator('.g3-date-picker__input').first().click()
await page.locator('.g3-calendar__title-button').first().click()
const selector = page.locator('.g3-calendar__selector')
await expect(selector).toBeVisible()
await expect(selector.locator('.g3-calendar__selector-item')).toHaveCount(12)
await expect(selector).toHaveCSS('grid-template-columns', /(\d+(\.\d+)?px ){2}/)
const title = await page.locator('.g3-calendar__title-button').first().textContent()
expect(title).toMatch(/\d{4}\s*-\s*\d{4}/)
await expect(selector.locator('.g3-calendar__selector-item.is-selected')).toHaveCount(1)
})
})
test.describe('Splitter 拖拽', () => {
test('面板宽度随指针线性变化(曾经一拖就跳)', async ({ page }) => {
await page.goto('/')
const bar = page.locator('.g3-splitter__bar').first()
// mouse API 用视口坐标且不会自动滚动,元素不在视口内时拖拽会落空
await bar.scrollIntoViewIfNeeded()
const box = (await bar.boundingBox())!
const widths = () =>
page.$$eval('.g3-splitter-panel', (els) =>
els.map((el) => Math.round(el.getBoundingClientRect().width)),
)
const before = await widths()
const startX = box.x + box.width / 2
const y = box.y + box.height / 2
await page.mouse.move(startX, y)
await page.mouse.down()
const samples: number[][] = []
for (const dx of [20, 40, 60]) {
await page.mouse.move(startX + dx, y)
await page.waitForTimeout(60)
samples.push(await widths())
}
await page.mouse.up()
// 每移动 20px,左侧面板就该宽 20px(严格 1:1,不是跳到某个值)
samples.forEach((sample, index) => {
const delta = sample[0] - before[0]
expect(delta, `第 ${index + 1} 次移动后左侧宽度增量`).toBeGreaterThanOrEqual((index + 1) * 20 - 2)
expect(delta).toBeLessThanOrEqual((index + 1) * 20 + 2)
})
})
})