225 lines
9.2 KiB
TypeScript
225 lines
9.2 KiB
TypeScript
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)
|
||
})
|
||
})
|
||
})
|