u
This commit is contained in:
1 parent
7bd5f1dde9
commit
f3936cec61
70 files changed
+13074
-187
No files matched your search
@@ -154,6 +154,292 @@ test.describe('G3Table 选择列(真浏览器命中)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 行内编辑的几何契约。
|
||||
*
|
||||
* happy-dom 没有布局引擎,这些只能在真浏览器里量:编辑器的盒高必须等于行高,
|
||||
* 且进入编辑**不得改变**所在行的高度(同行的纯文本列也不能跟着跳)。
|
||||
*/
|
||||
test.describe('G3Table 行内编辑:无外间距、无常驻边框', () => {
|
||||
/** 量表格里每一行的高度(取整) */
|
||||
async function rowHeights(page: import('@playwright/test').Page, wrapSelector: string) {
|
||||
return page.locator(wrapSelector).locator('.stk-tbody-main tr').evaluateAll((rows) =>
|
||||
rows.map((el) => Math.round(el.getBoundingClientRect().height)),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 按**列标题**定位单元格,不写死列序号。
|
||||
*
|
||||
* 写死 `td:nth-child(n)` 是错的来源:列顺序由调用方决定(演示数据里
|
||||
* 「备注」是第 7 列、「状态」是第 5 列),列一变断言就指到别的格子,
|
||||
* 表现为「莫名其妙失败」。
|
||||
*/
|
||||
async function cellByTitle(
|
||||
page: import('@playwright/test').Page,
|
||||
wrapSelector: string,
|
||||
title: string,
|
||||
) {
|
||||
const wrap = page.locator(wrapSelector)
|
||||
const index = await wrap
|
||||
.locator('thead th')
|
||||
.evaluateAll((ths, t) => ths.findIndex((th) => (th.textContent || '').trim() === t), title)
|
||||
if (index < 0) throw new Error(`找不到列标题「${title}」`)
|
||||
return wrap.locator('.stk-tbody-main tr').first().locator('td').nth(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器根节点选择器。
|
||||
*
|
||||
* ⚠️ 不能只用 `.g3-table__cell-editor-root`:checkbox 编辑器也用同一个类
|
||||
* (`label.g3-table__checkbox.g3-table__cell-editor-root`),演示数据里每行
|
||||
* 有两个复选框列,于是 strict mode 直接报「resolved to 3 elements」。
|
||||
* 这里只取 Input / Select 这两种「带外框的」编辑器。
|
||||
*/
|
||||
const EDITOR = '.g3-table__cell-editor-root.g3-input, .g3-table__cell-editor-root.g3-select'
|
||||
|
||||
test('根节点带 is-editor-seamless(默认开启)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
await expect(wrap.locator('.g3-table').first()).toHaveClass(/is-editor-seamless/)
|
||||
})
|
||||
|
||||
test('进入编辑:行高不变,且编辑器铺满单元格(无外间距)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
const before = await rowHeights(page, '[data-testid="table-wrap"]')
|
||||
|
||||
await (await cellByTitle(page, '[data-testid="table-wrap"]', '备注')).click()
|
||||
const editor = wrap.locator(EDITOR).first()
|
||||
await expect(editor).toBeVisible()
|
||||
|
||||
// 编辑器盒高 == 所在 td 的高(td 上下 padding 为 0 → 两者相等)
|
||||
const metrics = await editor.evaluate((el) => {
|
||||
const td = el.closest('td') as HTMLElement
|
||||
const style = getComputedStyle(el)
|
||||
const tdStyle = getComputedStyle(td)
|
||||
return {
|
||||
editor: Math.round(el.getBoundingClientRect().height),
|
||||
td: Math.round(td.getBoundingClientRect().height),
|
||||
tdPadTop: tdStyle.paddingTop,
|
||||
tdPadBottom: tdStyle.paddingBottom,
|
||||
border: style.borderTopWidth,
|
||||
radius: style.borderTopLeftRadius,
|
||||
background: style.backgroundColor,
|
||||
}
|
||||
})
|
||||
|
||||
// 无外间距:编辑器与格同高,td 垂直 padding 为 0(编辑器不被内缩)
|
||||
expect(metrics.editor).toBe(metrics.td)
|
||||
expect(metrics.tdPadTop).toBe('0px')
|
||||
expect(metrics.tdPadBottom).toBe('0px')
|
||||
// 无常驻边框 / 圆角 / 底色
|
||||
expect(metrics.border).toBe('0px')
|
||||
expect(metrics.radius).toBe('0px')
|
||||
expect(metrics.background).toBe('rgba(0, 0, 0, 0)')
|
||||
|
||||
// 关键回归:编辑后行高不变(历史 bug 是从 30px 变 42px)
|
||||
expect(await rowHeights(page, '[data-testid="table-wrap"]')).toEqual(before)
|
||||
})
|
||||
|
||||
test('Input 编辑器:内边距转移到内层原生 input,不落在外壳上', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
await (await cellByTitle(page, '[data-testid="table-wrap"]', '备注')).click()
|
||||
await expect(wrap.locator('.g3-table__cell-editor-root.g3-input').first()).toBeVisible()
|
||||
|
||||
const padding = await wrap
|
||||
.locator('.g3-table__cell-editor-root.g3-input')
|
||||
.first()
|
||||
.evaluate((el) => ({
|
||||
shell: getComputedStyle(el).padding,
|
||||
// 精确取原生控件;`.g3-input input` 会连 password/clear 按钮一起算进来
|
||||
control: getComputedStyle(el.querySelector('.g3-input__control') as HTMLElement).padding,
|
||||
}))
|
||||
|
||||
// 外壳无内边距 = 无外间距;网格间距由内层控件补回
|
||||
expect(padding.shell).toBe('0px')
|
||||
expect(padding.control).not.toBe('0px')
|
||||
})
|
||||
|
||||
test('焦点态才出现视觉提示:单元格内嵌焦点环', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
const target = await cellByTitle(page, '[data-testid="table-wrap"]', '备注')
|
||||
|
||||
// 未聚焦时该格没有内嵌焦点环
|
||||
const idle = await target.evaluate((el) => getComputedStyle(el).boxShadow)
|
||||
expect(idle).not.toContain('inset')
|
||||
|
||||
await target.click()
|
||||
await expect(wrap.locator(EDITOR).first()).toBeVisible()
|
||||
|
||||
// 焦点环画在**被点的那一格**上(限定 target,避免匹配到同行其它格)
|
||||
const focused = await target.evaluate((el) => getComputedStyle(el).boxShadow)
|
||||
expect(focused).toContain('inset')
|
||||
})
|
||||
|
||||
test('select 编辑器同样铺满:触发器的描边 / 圆角 / 32px 下限都被去掉', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
const before = await rowHeights(page, '[data-testid="table-wrap"]')
|
||||
|
||||
await (await cellByTitle(page, '[data-testid="table-wrap"]', '状态')).click()
|
||||
const select = wrap.locator('.g3-table__cell-editor-root.g3-select').first()
|
||||
await expect(select).toBeVisible()
|
||||
|
||||
/**
|
||||
* 关键:量**触发器**而不是组件根。
|
||||
*
|
||||
* 触发器的真实 DOM 位置是 `div.g3-select > [Popover] > span.g3-select__trigger`,
|
||||
* 不在根的直接子树里 —— 只量根会漏掉真正带描边/圆角的那个元素
|
||||
* (这正是此前「改了但看着没变」的原因)。
|
||||
*/
|
||||
const metrics = await select.evaluate((el) => {
|
||||
const trigger = el.querySelector('.g3-select__trigger') as HTMLElement
|
||||
const s = getComputedStyle(trigger)
|
||||
const root = getComputedStyle(el)
|
||||
return {
|
||||
rootClass: el.className,
|
||||
triggerHeight: Math.round(trigger.getBoundingClientRect().height),
|
||||
triggerBorder: s.borderTopWidth,
|
||||
triggerRadius: s.borderTopLeftRadius,
|
||||
triggerBg: s.backgroundColor,
|
||||
rootBorder: root.borderTopWidth,
|
||||
rootRadius: root.borderTopLeftRadius,
|
||||
}
|
||||
})
|
||||
|
||||
// 无缝修饰类必须真的落到 G3Select 根上(否则下面的样式规则不生效)
|
||||
expect(metrics.rootClass).toContain('is-seamless')
|
||||
|
||||
expect(metrics.triggerBorder).toBe('0px')
|
||||
expect(metrics.triggerRadius).toBe('0px')
|
||||
expect(metrics.triggerBg).toBe('rgba(0, 0, 0, 0)')
|
||||
expect(metrics.rootBorder).toBe('0px')
|
||||
expect(metrics.rootRadius).toBe('0px')
|
||||
|
||||
// 触发器不再锁 32px(行高 30px),否则会把行顶高 2px
|
||||
const rowHeight = await wrap
|
||||
.locator('.stk-tbody-main tr')
|
||||
.first()
|
||||
.evaluate((el) => Math.round(el.getBoundingClientRect().height))
|
||||
expect(metrics.triggerHeight).toBeLessThanOrEqual(rowHeight)
|
||||
|
||||
expect(await rowHeights(page, '[data-testid="table-wrap"]')).toEqual(before)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 校验态、列宽拖拽、当前行高亮 —— 这三条是本组件反复回归过的行为,
|
||||
* 专门用真浏览器锁住(happy-dom 没有布局引擎,也测不了 :has() 命中)。
|
||||
*
|
||||
* 宿主导入的 playground 数据里:
|
||||
* - 金额列 validate:> 0 否则 error(第 2 行 amount=0 → 必有一处 error)
|
||||
* - 备注列 validateWarning:非空否则 warning(第 2 行 remark='' → 必有一处 warning)
|
||||
* - table-wrap 绑了 v-model:columns(列宽拖拽的前提,缺了拖不动)
|
||||
*/
|
||||
test.describe('G3Table 校验态 / 列宽拖拽 / 行高亮', () => {
|
||||
test('校验态全表可见:不点任何行就能看到 error 与 warning', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
// 关键前提:进入页面后**不点击任何单元格**,直接断言校验态存在。
|
||||
// 这锁的是「校验是数据的状态,不依赖哪一行被交互过」——此前 text/number
|
||||
// 列在非编辑态走库原生 td,校验态根本不渲染(必须点行才出现)。
|
||||
const errCells = wrap.locator('td:has(> .is-editor-error)')
|
||||
const warnCells = wrap.locator('td:has(> .is-editor-warning)')
|
||||
await expect(errCells).toHaveCount(1) // 金额列第 2 行 amount=0
|
||||
await expect(warnCells).toHaveCount(1) // 备注列第 2 行 remark=''
|
||||
})
|
||||
|
||||
test('校验态落在正确的列上(error=金额、warning=备注)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
// 用列标题定位,不写死索引
|
||||
const ths = await wrap.locator('thead th').evaluateAll((all) =>
|
||||
all.map((t) => (t.textContent || '').trim()),
|
||||
)
|
||||
const amountIdx = ths.findIndex((t) => t === '金额')
|
||||
const remarkIdx = ths.findIndex((t) => t === '备注')
|
||||
|
||||
// 第 2 行(合同 B)的金额格是 error、备注格是 warning
|
||||
const row = wrap.locator('.stk-tbody-main tr').nth(1)
|
||||
await expect(row.locator('td').nth(amountIdx).locator('.is-editor-error')).toHaveCount(1)
|
||||
await expect(row.locator('td').nth(remarkIdx).locator('.is-editor-warning')).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('列宽拖拽:绑定 v-model:columns 后可拖动,宽度真实变化', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
// 表格应带 col-resizable 类(列宽拖拽已启用)
|
||||
await expect(wrap.locator('.stk-table')).toHaveClass(/col-resizable/)
|
||||
|
||||
// 取「名称」列表头宽度(第 2 个 th,序号列是第 1 个)
|
||||
const th = wrap.locator('thead th').nth(2)
|
||||
const before = await th.evaluate((el) => Math.round(el.getBoundingClientRect().width))
|
||||
|
||||
// 拖它的右边缘手柄 +60px
|
||||
const handle = th.locator('.table-header-resizer.right')
|
||||
const hb = await handle.boundingBox()
|
||||
expect(hb).not.toBeNull()
|
||||
if (hb) {
|
||||
const cx = hb.x + hb.width / 2
|
||||
const cy = hb.y + hb.height / 2
|
||||
await page.mouse.move(cx, cy)
|
||||
await page.mouse.down()
|
||||
for (let i = 1; i <= 15; i += 1) {
|
||||
await page.mouse.move(cx + i * 4, cy)
|
||||
await page.waitForTimeout(15)
|
||||
}
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(500)
|
||||
}
|
||||
|
||||
const after = await th.evaluate((el) => Math.round(el.getBoundingClientRect().width))
|
||||
expect(after).toBeGreaterThan(before)
|
||||
})
|
||||
|
||||
test('点击行进入编辑时,当前行高亮(active)跟随', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
const rows = wrap.locator('.stk-tbody-main tr')
|
||||
|
||||
// 初始无 active 行
|
||||
const activeCount = async () =>
|
||||
wrap.locator('.stk-tbody-main tr.active').count()
|
||||
expect(await activeCount()).toBe(0)
|
||||
|
||||
// 点第 2 行的一个可编辑格(金额列),进入编辑并高亮
|
||||
const ths = await wrap.locator('thead th').evaluateAll((all) =>
|
||||
all.map((t) => (t.textContent || '').trim()),
|
||||
)
|
||||
const amountIdx = ths.findIndex((t) => t === '金额')
|
||||
await rows.nth(1).locator('td').nth(amountIdx).click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// 第 2 行应带 active
|
||||
await expect(rows.nth(1)).toHaveClass(/active/)
|
||||
expect(await activeCount()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 这两条盯的是「库的 flex 布局 + 自绘分隔线」这对组合:
|
||||
* 库的 DOM 里 table.stk-table-main 是 .stk-table-scroll-container 的 flex item,
|
||||
|
||||
Reference in new issue
Block a user