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

259 lines
10 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'
import zlib from 'node:zlib'
/**
* 最小 PNG 解码(8bit,colorType 2/6,非隔行)。
* 用于「读真实渲染像素」——只量 computed 样式会漏掉「声明正确但没画出来」
* 这类问题(表头半透明底色最初就是这么漏过去的)。
*/
function decodePng(buf: Buffer) {
let pos = 8
let width = 0
let height = 0
let colorType = 6
const idat: Buffer[] = []
while (pos < buf.length) {
const len = buf.readUInt32BE(pos)
const type = buf.toString('ascii', pos + 4, pos + 8)
const data = buf.subarray(pos + 8, pos + 8 + len)
if (type === 'IHDR') {
width = data.readUInt32BE(0)
height = data.readUInt32BE(4)
colorType = data[9]
} else if (type === 'IDAT') idat.push(data)
pos += 12 + len
}
const channels = colorType === 6 ? 4 : 3
const raw = zlib.inflateSync(Buffer.concat(idat))
const bpp = channels
const stride = width * bpp
const out = Buffer.alloc(height * stride)
let rp = 0
for (let y = 0; y < height; y += 1) {
const filter = raw[rp]
rp += 1
const rs = y * stride
for (let x = 0; x < stride; x += 1) {
const cur = raw[rp + x]
const left = x >= bpp ? out[rs + x - bpp] : 0
const up = y > 0 ? out[rs - stride + x] : 0
const ul = y > 0 && x >= bpp ? out[rs - stride + x - bpp] : 0
let v = cur
if (filter === 1) v = cur + left
else if (filter === 2) v = cur + up
else if (filter === 3) v = cur + ((left + up) >> 1)
else if (filter === 4) {
const p = left + up - ul
const pa = Math.abs(p - left)
const pb = Math.abs(p - up)
const pc = Math.abs(p - ul)
v = cur + (pa <= pb && pa <= pc ? left : pb <= pc ? up : ul)
}
out[rs + x] = v & 0xff
}
rp += stride
}
return { width, height, pixels: out, bpp }
}
/** 取图片主色(跳过 1px 边缘,避开边框线),返回 "r,g,b" */
function dominantColor(img: ReturnType<typeof decodePng>): string {
const { width, height, pixels, bpp } = img
const hist = new Map<string, number>()
for (let y = 1; y < height - 1; y += 1) {
for (let x = 1; x < width - 1; x += 1) {
const i = (y * width + x) * bpp
const k = pixels[i] + ',' + pixels[i + 1] + ',' + pixels[i + 2]
hist.set(k, (hist.get(k) ?? 0) + 1)
}
}
return [...hist.entries()].sort((a, b) => b[1] - a[1])[0][0]
}
/**
* 文档站上 G3Table 的渲染核对。
*
* 放在独立的 e2e-docs 目录、用 playwright.docs.config.ts 跑:
* 组件自带的 e2e/ 指向 playground,那里没有 VitePress 的 .vp-doc 上下文,
* 量不到文档页才有的样式问题(前几轮反复踩坑就在这)。
*/
test.describe('文档站 /ui/data/table', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/ui/data/table')
await page.waitForTimeout(1000)
})
test('七个示例都渲染出表格', async ({ page }) => {
// basic / virtual / selection / sorting / editable / actions / resizable(+2 表)
const tables = page.locator('.g3-table')
const count = await tables.count()
expect(count).toBeGreaterThanOrEqual(7)
})
test('没有 Vue 渲染告警泄漏到页面(控制台无 error)', async ({ page }) => {
const errors: string[] = []
page.on('pageerror', (e) => errors.push(e.message))
await page.goto('/ui/data/table')
await page.waitForTimeout(800)
expect(errors).toEqual([])
})
test('内部边框是 1px:逐像素量横线带与竖线宽', async ({ page }) => {
const table = page.locator('.g3-table').first()
await table.scrollIntoViewIfNeeded()
await page.waitForTimeout(300)
// 用 canvas 采样不方便,这里退一步量计算样式:
// 库用 background-image 画线,.vp-doc 会用 border 画四边框 —— 后者必须被清掉。
const cell = table.locator('.stk-tbody-main td').first()
const styles = await cell.evaluate((el) => {
const cs = getComputedStyle(el)
return {
borderTop: cs.borderTopWidth,
borderBottom: cs.borderBottomWidth,
borderLeft: cs.borderLeftWidth,
borderRight: cs.borderRightWidth,
bg: cs.backgroundImage,
}
})
// .vp-doc 的四边框必须已被 demo 隔离规则清掉
expect(styles.borderTop).toBe('0px')
expect(styles.borderBottom).toBe('0px')
expect(styles.borderLeft).toBe('0px')
expect(styles.borderRight).toBe('0px')
// 库自己的线仍在(background-image 里至少一条可见渐变)
expect(styles.bg).not.toBe('none')
expect(styles.bg).toContain('gradient')
})
test('「名称长度(自定义)」列显示数字而不是名称,且点表头能排序', async ({ page }) => {
const table = page
.locator('.g3-table')
.filter({ has: page.locator('th', { hasText: '名称长度' }) })
.first()
await table.scrollIntoViewIfNeeded()
const headers = await table.locator('thead th').allTextContents()
const idx = headers.findIndex((h) => h.includes('名称长度')) + 1
const read = async () =>
(await table.locator(`.stk-tbody-main tr td:nth-child(${idx})`).allTextContents()).map((s) =>
s.trim(),
)
// 该列必须显示**长度值**(dataIndex 指向真实字段),不能显示名称本身
const before = await read()
expect(before).toEqual(['4', '8', '3'])
await table.locator('th', { hasText: '名称长度' }).first().click()
await page.waitForTimeout(300)
expect(await read()).toEqual(['8', '4', '3']) // desc
await table.locator('th', { hasText: '名称长度' }).first().click()
await page.waitForTimeout(300)
expect(await read()).toEqual(['3', '4', '8']) // asc
})
/**
* 虚拟滚动开启时表头必须钉住。
*
* 这条专门防「宿主 CSS 泄漏」回归:VitePress 的
* `.vp-doc table { display:block; overflow-x:auto; margin:20px 0 }`
* 会命中组件内部的 `<table class="stk-table-main">`,让它变成滚动容器,
* 于是 th 的 sticky 基准错位、表头跟着滚走(实测 delta=-600)。
* custom.css 里已把这几个属性还原,这里用位移量锁住。
*
* 注意 playground 的同类断言抓不到这个 bug —— 那里没有 .vp-doc。
*/
test('虚拟滚动:纵向滚动 600px 后表头仍钉在容器顶部', async ({ page }) => {
await page.goto('/ui/data/table')
await page.waitForTimeout(1000)
const section = page
.locator('.vitepress-demo-plugin__container')
.filter({ has: page.locator('th', { hasText: '数值' }) })
.first()
await section.scrollIntoViewIfNeeded()
await page.waitForTimeout(300)
const r = await page.evaluate(() => {
const stk = document.querySelector('.stk-table.virtual') as HTMLElement | null
if (!stk) return null
const th = stk.querySelector('thead th') as HTMLElement
const top0 = stk.getBoundingClientRect().top
const before = th.getBoundingClientRect().top - top0
stk.scrollTop = 600
const after = th.getBoundingClientRect().top - top0
return { before, after, delta: after - before, scrollTop: stk.scrollTop }
})
expect(r).not.toBeNull()
expect(r!.scrollTop).toBe(600)
// 表头相对滚动容器顶部的偏移不应变化
expect(Math.abs(r!.delta)).toBeLessThan(2)
})
/**
* 表头底色必须不透明。
*
* 库把表头底色做成 alpha 值(--th-bgc: rgba(0,0,0,.04))画在 thead tr 上、
* th 继承它;th 又是 sticky,滚动时表体行从它下面高速掠过,半透明像素会与
* 「此刻经过的那一行」实时合成 —— 表现为滚动时能看到行内容一闪而过。
* 静止截图看不出(下方恰好是白行),所以这里主动塞一行纯红到表头正下方,
* 再截图表头:若还能读出红色,说明底色仍是半透明。
*
* 注意:这条同样只有文档站能跑出意义 —— playground 不会有 .vp-doc 干扰,
* 但真正要防的是「表头底色被改回 alpha」,两边都成立;放这里是为了顺带
* 覆盖文档站的真实渲染。
*/
test('表头底色不透明:下方内容不会透出来', async ({ page }) => {
await page.goto('/ui/data/table')
await page.waitForTimeout(1000)
const section = page
.locator('.vitepress-demo-plugin__container')
.filter({ has: page.locator('th', { hasText: '数值' }) })
.first()
await section.scrollIntoViewIfNeeded()
await page.waitForTimeout(400)
const table = section.locator('.g3-table').first()
// 1) alpha 必须是 1
const alpha = await table.evaluate((root) => {
const th = root.querySelector('thead th') as HTMLElement
const c = getComputedStyle(th).backgroundColor
const m = c.match(/rgba?\(([^)]+)\)/)
if (!m) return null
const parts = m[1].split(',').map((p) => p.trim())
return { bg: c, alpha: parts.length > 3 ? Number(parts[3]) : 1 }
})
expect(alpha).not.toBeNull()
expect(alpha!.alpha).toBe(1)
// 2) 行为验证:把表体行染成纯红、滚到表头正下方,截图表头 —— 读像素判断。
// 只量 computed 是不够的(正是那种检查漏掉了原 bug),必须看真实渲染像素。
await table.evaluate((root) => {
const rows = root.querySelectorAll('.stk-tbody-main tr')
const r = rows[0] as HTMLElement | undefined
if (r) {
r.querySelectorAll('td').forEach((td) => {
;(td as HTMLElement).style.backgroundColor = 'rgb(255,0,0)'
;(td as HTMLElement).style.backgroundImage = 'none'
})
}
const stk = root.querySelector('.stk-table') as HTMLElement
stk.scrollTop = 1
})
await page.waitForTimeout(400)
const shot = await table.locator('thead th').first().screenshot()
const color = dominantColor(decodePng(Buffer.from(shot)))
const [r, g, b] = color.split(',').map(Number)
// 表头主色不应偏红 —— 红行若透上来,R 会显著高于 G/B
expect(r - Math.max(g, b)).toBeLessThan(20)
})
})