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) }) })