250 lines
11 KiB
TypeScript
250 lines
11 KiB
TypeScript
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)
|
||
})
|
||
})
|