Files
workspace/code/g3soft-libs/packages/ui/e2e/table.spec.ts
T
2026-10-12 22:25:41 +08:00

250 lines
11 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'
/**
* G3Table 的几何 / 命中类断言 —— happy-dom 没有布局引擎,这些只能在真浏览器里量。
*
* 用例只按 data-testid 找元素(不依赖文案与结构),宿主页在 playground/main.ts。
*/
test.describe('G3Table 表格', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.locator('[data-testid="table"]').scrollIntoViewIfNeeded()
})
test('基础渲染:表头与数据行都在,且序号列存在', async ({ page }) => {
const table = page.locator('[data-testid="table-wrap"]')
await expect(table.locator('.stk-table')).toBeVisible()
// 表头至少有我们的业务列文案
await expect(table.locator('th', { hasText: '名称' })).toBeVisible()
await expect(table.locator('th', { hasText: '金额' })).toBeVisible()
// 数据行渲染出来了
const rows = table.locator('.stk-tbody-main tr')
await expect(rows).toHaveCount(3)
})
test('小数据量(3 行):纵向虚拟滚动关闭,DOM 里是真实 3 行', async ({ page }) => {
const table = page.locator('[data-testid="table-wrap"]')
// 库开启虚拟滚动时会给根加 .virtual 类;门控生效时不应有
await expect(table.locator('.stk-table')).not.toHaveClass(/virtual/)
})
test('大数据量(5000 行):纵向虚拟滚动开启,DOM 行数远少于数据量', async ({ page }) => {
const big = page.locator('[data-testid="table-big-wrap"]')
await big.scrollIntoViewIfNeeded()
await expect(big.locator('.stk-table')).toHaveClass(/virtual/)
// 虚拟滚动的意义:只渲染视口内若干行,DOM 行数应当是个位数/几十,而不是 5000
const rendered = await big.locator('.stk-tbody-main tr').count()
expect(rendered).toBeGreaterThan(0)
expect(rendered).toBeLessThan(200)
})
test('大数据量:滚动后仍只渲染视口内行(虚拟窗口跟随)', async ({ page }) => {
const big = page.locator('[data-testid="table-big-wrap"]')
await big.scrollIntoViewIfNeeded()
const scroller = big.locator('.stk-table-scroll-container, .stk-table-main').first()
await scroller.evaluate((el) => {
el.scrollTop = 3000
})
await page.waitForTimeout(150)
const rendered = await big.locator('.stk-tbody-main tr').count()
expect(rendered).toBeGreaterThan(0)
expect(rendered).toBeLessThan(200)
})
test('高度约束:容器限高时表格不溢出', async ({ page }) => {
const wrap = page.locator('[data-testid="table-wrap"]')
const wrapBox = await wrap.boundingBox()
const inner = wrap.locator('.stk-table').first()
const innerBox = await inner.boundingBox()
expect(wrapBox).not.toBeNull()
expect(innerBox).not.toBeNull()
// 表格不应高于它的容器(留 2px 容差:边框/亚像素)
expect(innerBox!.height).toBeLessThanOrEqual(wrapBox!.height + 2)
})
test('边框由库绘制:不再有自绘外框与圆角', async ({ page }) => {
// 组件已改为把边框完全交给库(传 bordered: true),自己不再画外框 / 圆角。
// data-testid 透传给内层 StkTable(组件 inheritAttrs: false),所以按类名定位根节点。
const root = page.locator('[data-testid="table-wrap"] .g3-table').first()
await expect(root).toHaveClass(/is-bordered/)
// 自绘外框已删除:根节点不该再有 border / 圆角
const radius = await root.evaluate((el) => getComputedStyle(el).borderRadius)
expect(radius).toBe('0px')
const borderWidth = await root.evaluate((el) => getComputedStyle(el).borderTopWidth)
expect(borderWidth).toBe('0px')
// 边框改由库的 .bordered 提供
await expect(page.locator('[data-testid="table-wrap"] .stk-table').first()).toHaveClass(
/bordered/,
)
})
test('表头显示序号列(seq 默认开启)', async ({ page }) => {
const table = page.locator('[data-testid="table-wrap"]')
await expect(table.locator('th', { hasText: '序号' })).toBeVisible()
})
test('窄容器下不横向溢出容器宽度', async ({ page }) => {
const wrap = page.locator('[data-testid="table-narrow-wrap"]')
await wrap.scrollIntoViewIfNeeded()
const wrapBox = await wrap.boundingBox()
const rootBox = await wrap.locator('[data-testid="table-narrow"]').boundingBox()
expect(rootBox!.width).toBeLessThanOrEqual(wrapBox!.width + 2)
})
})
test.describe('G3Table 选择列(真浏览器命中)', () => {
test('点击行复选框:勾选态可见地变化', async ({ page }) => {
await page.goto('/')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
const firstRowCheckbox = wrap.locator('.g3-table__checkbox').first()
await firstRowCheckbox.scrollIntoViewIfNeeded()
await expect(firstRowCheckbox).not.toHaveClass(/is-checked/)
await firstRowCheckbox.click()
await expect(firstRowCheckbox).toHaveClass(/is-checked/)
})
test('每行复选框数 = 选择列 + checkbox 编辑器列(演示数据定义了两列)', async ({ page }) => {
await page.goto('/')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
// 演示数据的 tableColumns 里有两个复选框列:选择列(注入)+ 启用列(editor: checkbox)。
// 所以每行 2 个是预期值——这正是为什么不能靠「数复选框个数」判断是否有重复渲染。
const bodyRows = wrap.locator('.stk-tbody-main tr')
await expect(bodyRows).toHaveCount(3)
for (let i = 0; i < 3; i += 1) {
await expect(bodyRows.nth(i).locator('.g3-table__checkbox')).toHaveCount(2)
}
})
test('无重复渲染:每行 td 数等于表头列数', async ({ page }) => {
await page.goto('/')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
// 单元格数必须与表头列数一致;固定列若被双份渲染,这里会多出来。
const headerCount = await wrap.locator('thead th').count()
const firstRowTd = await wrap.locator('.stk-tbody-main tr').first().locator('td').count()
expect(firstRowTd).toBe(headerCount)
})
test('表头全选:点击后表头与所有数据行的选择列都被勾选', async ({ page }) => {
await page.goto('/')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
const headerCheckbox = wrap.locator('th .g3-table__checkbox').first()
await headerCheckbox.scrollIntoViewIfNeeded()
await headerCheckbox.click()
// 表头 choice 项 + 3 行选择列 = 4 个处于勾选态的选择框
// (「启用」列的 checkbox 是数据值,初始为 1 的那两行也会是勾选态,
// 所以这里按选择列的固定类名限定范围)
await expect(wrap.locator('th .g3-table__checkbox.is-checked')).toHaveCount(1)
await expect(wrap.locator('.stk-tbody-main td:first-child .g3-table__checkbox.is-checked')).toHaveCount(3)
})
})
/**
* 这两条盯的是「库的 flex 布局 + 自绘分隔线」这对组合:
* 库的 DOM 里 table.stk-table-main 是 .stk-table-scroll-container 的 flex item,
* 库默认给 flex:1,而表头 th 靠 position:sticky 固定,一旦 table 被拉伸,
* sticky 几何就会失效(表头跟着滚、分隔线错位叠线)。
*/
test.describe('G3Table 布局:表头固定与分隔线', () => {
test('table 不被拉伸:.stk-table-main 是 flex:none(非 autoFitWidth)', async ({ page }) => {
await page.goto('/')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
const flex = await wrap
.locator('.stk-table-main')
.first()
.evaluate((el) => getComputedStyle(el).flexGrow)
expect(flex).toBe('0')
})
test('滚动后表头仍固定在容器顶部', async ({ page }) => {
await page.goto('/')
const big = page.locator('[data-testid="table-big-wrap"]')
await big.scrollIntoViewIfNeeded()
const header = big.locator('thead th').first()
const before = await header.boundingBox()
// 滚动表体
const scroller = big.locator('.stk-table-scroll-container').first()
await scroller.evaluate((el) => {
el.scrollTop = 1200
})
await page.waitForTimeout(200)
const after = await header.boundingBox()
expect(before).not.toBeNull()
expect(after).not.toBeNull()
// 表头是 sticky:滚动前后它相对视口的位置不应发生位移
expect(Math.abs(after!.y - before!.y)).toBeLessThan(2)
})
test('边框交由库绘制:库根节点带 .bordered,且不再叠加自绘实线', async ({ page }) => {
await page.goto('/')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
// 我们传 bordered: true,库会给根节点挂 .bordered(并因是字符串才挂 bordered-xxx)
const stk = wrap.locator('.stk-table').first()
await expect(stk).toHaveClass(/bordered/)
// 自绘的行分隔线已删除,单元格不应再有真的 border-bottom
// (库用 background-image 渐变画线,不走 border)
const td = await wrap
.locator('.stk-tbody-main td')
.first()
.evaluate((el) => getComputedStyle(el).borderBottomWidth)
expect(td).toBe('0px')
})
test('库自绘边框:单元格带可见的横竖渐变线', async ({ page }) => {
await page.goto('/')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
// 库的 .bordered th,td 用 background-image 画 --bg-border-right / --bg-border-bottom。
// 这里只断言「至少有一条可见渐变线」,不锁具体层数——那是库的内部实现。
const layers = await wrap
.locator('.stk-tbody-main td')
.first()
.evaluate((el) => {
const bg = getComputedStyle(el).backgroundImage
return bg
.split(/,(?![^(]*\))/)
.map((s) => s.trim())
.filter((s) => /rgb\(\d+, \d+, \d+\)/.test(s) && !/rgba\(0, 0, 0, 0\)/.test(s)).length
})
expect(layers).toBeGreaterThan(0)
})
})
test.describe('G3Table RTL 镜像', () => {
test('RTL 下固定列与表格布局不塌陷', async ({ page }) => {
await page.goto('/?dir=rtl')
const wrap = page.locator('[data-testid="table-wrap"]')
await wrap.scrollIntoViewIfNeeded()
const wrapBox = await wrap.boundingBox()
const tableBox = await wrap.locator('.stk-table').first().boundingBox()
expect(tableBox).not.toBeNull()
// 表格仍应占据容器的整宽(镜像不该把宽度算丢)
expect(tableBox!.width).toBeGreaterThan(wrapBox!.width * 0.9)
})
})