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('外框圆角:bordered 时容器有圆角与描边', async ({ page }) => { // 注意:data-testid 透传给了内层 StkTable(组件 inheritAttrs: false), // 外框与圆角在我们自己的根节点 .g3-table 上,所以要按类名定位。 const root = page.locator('[data-testid="table-wrap"] .g3-table').first() await expect(root).toHaveClass(/is-bordered/) const radius = await root.evaluate((el) => getComputedStyle(el).borderRadius) expect(radius).not.toBe('0px') const borderWidth = await root.evaluate((el) => getComputedStyle(el).borderTopWidth) expect(borderWidth).toBe('1px') }) 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) }) }) 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) }) })