u
This commit is contained in:
1 parent
bf9dca19de
commit
e7c84a82a2
31 files changed
+4063
-2362
No files matched your search
@@ -297,6 +297,7 @@ test.describe('G3Table 行内编辑:无外间距、无常驻边框', () => {
|
||||
|
||||
const before = await rowHeights(page, '[data-testid="table-wrap"]')
|
||||
|
||||
// 双击进编辑(表格是「单击选中、进编辑另有其键」的两态模型,见 startEdit 的说明)
|
||||
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()
|
||||
@@ -484,6 +485,105 @@ test.describe('G3Table 校验态 / 列宽拖拽 / 行高亮', () => {
|
||||
await expect(row.locator('td').nth(remarkIdx).locator('.is-editor-warning')).toHaveCount(1)
|
||||
})
|
||||
|
||||
/**
|
||||
* 失败原因必须能到达用户。
|
||||
*
|
||||
* 背景:`resolveEditorStatus` 早先只取校验结果字符串的 `.length` 判等级、把内容丢掉 ——
|
||||
* 业务在 `validate` 里写的失败原因(「金额必须大于 0」)因此没有任何出口,
|
||||
* 用户只知道「这格红了」,不知道要改成什么。
|
||||
*
|
||||
* 这条从「内容节点上的 data 属性」一路验到「浮层真的弹出来」:属性没挂、
|
||||
* 委托没接上、浮层被表格的 `overflow` 裁掉、文本没绑 —— 任何一环被改断都会红。
|
||||
*/
|
||||
test('失败原因浮层:hover 出错格弹出 validate 的原因,移到普通格即收', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
const rows = wrap.locator('.stk-tbody-main tr')
|
||||
const errSpan = rows.nth(1).locator('td[data-col-key="amount"] .g3-table__cell-display')
|
||||
const warnSpan = rows.nth(1).locator('td[data-col-key="remark"] .g3-table__cell-display')
|
||||
|
||||
// 原因挂在内容节点上(不是 td —— 按行变化的值打不到 td 上,见 editable.ts 的说明)
|
||||
await expect(errSpan).toHaveAttribute('data-validate-message', '金额必须大于 0')
|
||||
await expect(warnSpan).toHaveAttribute('data-validate-message', '建议填写备注')
|
||||
|
||||
// 常态下浮层压根不在 DOM 里(`v-if`)—— 这是「全表共用一个、常态零成本」的直接证据
|
||||
const tip = page.locator('.g3-table__cell-tip')
|
||||
await expect(tip).toHaveCount(0)
|
||||
|
||||
await errSpan.hover()
|
||||
await expect(tip).toBeVisible()
|
||||
await expect(tip).toHaveText('金额必须大于 0')
|
||||
|
||||
// 移到同一行的普通格(第 1 行金额是正常值):浮层立刻收掉 ——
|
||||
// 不能一直挂着上一格的原因
|
||||
await rows.nth(0).locator('td[data-col-key="amount"]').hover()
|
||||
await expect(tip).toHaveCount(0)
|
||||
|
||||
/**
|
||||
* warning 走同一条通道,且**悬停格子(td)而不是那个 span**:
|
||||
* 备注在那一行是空值,展示 span 宽度为 0,鼠标落在 td 的 padding 上 ——
|
||||
* 这正是「按事件目标的祖先链找消息节点」会静默失效的位置。
|
||||
* 委托改为以格子为单位反查后,这里必须能弹出来。
|
||||
*/
|
||||
await rows.nth(1).locator('td[data-col-key="remark"]').hover()
|
||||
await expect(tip).toHaveText('建议填写备注')
|
||||
})
|
||||
|
||||
/**
|
||||
* 数值精度(`editor.precision`)的完整行为:按精度格式化展示、超精度只标红、
|
||||
* 且**不改数据**。
|
||||
*
|
||||
* playground 的「金额」列配了 `precision: 2` 与 `format: { thousands: true, prefix: '¥' }`。
|
||||
* 这里刻意用**动态输入**造超精度值,而不是往宿主的静态数据里塞一行 ——
|
||||
* 静态数据一改就会连带影响同页其它「error 格计数」的断言,动态输入只在本次用例内生效。
|
||||
*/
|
||||
test('数值精度:按精度格式化展示;输多了只标红、值原样保留', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
|
||||
const cell = wrap.locator('.stk-tbody-main tr').first().locator('td[data-col-key="amount"]')
|
||||
|
||||
// ① 展示:按 precision 补足到两位小数,叠加 format 的千分位与 ¥ 前缀
|
||||
await expect(cell.locator('.g3-table__cell-display')).toHaveText('¥12,000.00')
|
||||
|
||||
/**
|
||||
* ② 输入一个超精度的值,再点**另一个数据格**让它失焦提交。
|
||||
*
|
||||
* 这里不能点表头:编辑态的退出是由单元格选区变化驱动的,点表头不会退出编辑 ——
|
||||
* 那样这一格还挂着编辑器,下面针对「展示态 span」的断言会直接找不到元素。
|
||||
*/
|
||||
await cell.click()
|
||||
await cell.locator('input').fill('1.2345')
|
||||
await wrap.locator('.stk-tbody-main tr').nth(2).locator('td[data-col-key="amount"]').click()
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// ③ 标红,原因是**组件内置**的(业务在这一列没写任何与精度有关的校验)
|
||||
await expect(cell.locator('.g3-table__cell-display')).toHaveClass(/is-editor-error/)
|
||||
await cell.hover()
|
||||
await expect(page.locator('.g3-table__cell-tip')).toHaveText('最多 2 位小数')
|
||||
|
||||
/**
|
||||
* ④ 超精度时**不做格式化展示**:按两位格式化成 `¥1.23` 会让屏幕看起来完全合规、
|
||||
* 数据却不合规,用户永远发现不了自己错了 —— 所以这里必须是原样的 `1.2345`。
|
||||
*/
|
||||
await expect(cell.locator('.g3-table__cell-display')).toHaveText('1.2345')
|
||||
|
||||
// ⑤ 值也没被四舍五入:再点开编辑,输入框里还是用户敲的那串
|
||||
await cell.click()
|
||||
await expect(cell.locator('input')).toHaveValue('1.2345')
|
||||
|
||||
/**
|
||||
* ⑥ 编辑器的文字对齐**跟随列**(金额列是 `align: 'right'`)。
|
||||
*
|
||||
* 表单控件的 UA 样式把继承来的 `text-align` 覆盖成 `start`,不显式接上就会
|
||||
* 「展示态靠右、一点进编辑就跳到左边」,光标也跟着跑到最左 —— 进出编辑的位移很扎眼。
|
||||
*/
|
||||
await expect(cell.locator('input')).toHaveCSS('text-align', 'right')
|
||||
})
|
||||
|
||||
/**
|
||||
* 编辑态让位:正在编辑的校验格**不铺校验底色**,底色回到基色。
|
||||
*
|
||||
@@ -603,6 +703,43 @@ test.describe('G3Table 校验态 / 列宽拖拽 / 行高亮', () => {
|
||||
* sticky 几何就会失效(表头跟着滚、分隔线错位叠线)。
|
||||
*/
|
||||
test.describe('G3Table 布局:表头固定与分隔线', () => {
|
||||
/**
|
||||
* 按**顶层**逗号切分 CSS 列表值(`background-image` / `-size` / `-position` 都是这种)。
|
||||
*
|
||||
* ⚠ 不要写成 `split(/,(?![^(]*\))/)`:那种写法靠「逗号后面是否紧跟一个 `)`」判断,
|
||||
* 一旦逗号后面是 `rgba(` 这种带括号的值就失灵 —— 括号把 `[^(]*` 截断,负向前瞻
|
||||
* 随之成立,于是渐变**内部的**逗号(如 `linear-gradient(rgba(0,0,0,.08), rgba(0,0,0,.08))`
|
||||
* 两个色标之间那个)被当成层分隔符,一层被切成两段(实测把「两条线」数成三条)。
|
||||
* 括号配对计数才是可靠判据。
|
||||
*/
|
||||
function splitCssLayers(value: string): string[] {
|
||||
const out: string[] = []
|
||||
let depth = 0
|
||||
let start = 0
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const ch = value[i]
|
||||
if (ch === '(') depth++
|
||||
else if (ch === ')') depth--
|
||||
else if (ch === ',' && depth === 0) {
|
||||
out.push(value.slice(start, i).trim())
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
out.push(value.slice(start).trim())
|
||||
return out.filter(Boolean)
|
||||
}
|
||||
|
||||
/** 数「真的画得出颜色」的背景层:rgb(...) 或 alpha > 0 的 rgba(...) */
|
||||
async function visibleLineCount(locator: import('@playwright/test').Locator) {
|
||||
const bg = await locator.evaluate((el) => getComputedStyle(el).backgroundImage)
|
||||
return splitCssLayers(bg).filter((layer) =>
|
||||
[...layer.matchAll(/rgba?\(([^)]+)\)/g)].some((m) => {
|
||||
const parts = m[1].split(',').map((s) => Number(s.trim()))
|
||||
return parts.length < 4 || parts[3] > 0
|
||||
}),
|
||||
).length
|
||||
}
|
||||
|
||||
test('table 不被拉伸:.stk-table-main 是 flex:none(非 autoFitWidth)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
@@ -637,7 +774,7 @@ test.describe('G3Table 布局:表头固定与分隔线', () => {
|
||||
expect(Math.abs(after!.y - before!.y)).toBeLessThan(2)
|
||||
})
|
||||
|
||||
test('边框自绘:库只保留竖线,横线走渐变而不是真 border', async ({ page }) => {
|
||||
test('边框自绘:表体只剩横线走渐变,而不是真 border', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
@@ -646,25 +783,22 @@ test.describe('G3Table 布局:表头固定与分隔线', () => {
|
||||
const stk = wrap.locator('.stk-table').first()
|
||||
await expect(stk).toHaveClass(/bordered-v/)
|
||||
|
||||
/** 数「非透明的背景渐变层数」——即这一格画了几条线(不看库内部怎么拼) */
|
||||
const lineCount = (locator: import('@playwright/test').Locator) =>
|
||||
locator.evaluate((el) =>
|
||||
getComputedStyle(el)
|
||||
.backgroundImage.split(/,(?![^(]*\))/)
|
||||
.filter((layer) => /rgb\(\d+, \d+, \d+\)/.test(layer)).length,
|
||||
)
|
||||
|
||||
// 横线:`'v'` 档把 --bg-border-bottom 清零(只剩竖线一条),组件按底座配方把它
|
||||
// 拿回来 —— 所以这里应当数出**两条**。但它**不能**是真 border:格线一进盒模型,
|
||||
// 单元格多 1px 就会被行内编辑器顶高(行高是虚拟滚动 translateY 的基准)。
|
||||
// 表体的竖线已被覆盖层涂透明(--bg-border-right / --bg-border-left),所以表体
|
||||
// 每一格只剩**一条**可见渐变:组件按底座配方拿回来的那条横线。
|
||||
// (表头是例外 —— 列界画在 th 的 background-image 上,见下一条用例。)
|
||||
const td = wrap.locator('.stk-tbody-main td').first()
|
||||
const normalLines = await lineCount(td)
|
||||
expect(normalLines).toBeGreaterThan(1)
|
||||
expect(await td.evaluate((el) => getComputedStyle(el).borderBottomWidth)).toBe('0px')
|
||||
const normalLines = await visibleLineCount(td)
|
||||
expect(normalLines).toBe(1)
|
||||
|
||||
// 竖线:末列的外侧那条已被抹掉(归自绘外框管),所以比普通列**正好少一条**
|
||||
// 末列不再「少一条」:表体竖线既然全关,末列与普通列的层数必须相等
|
||||
// (旧断言是 normalLines - 1,那是在摘「最右列的外侧竖线」——该需求已不存在)
|
||||
const lastTd = wrap.locator('.stk-tbody-main td:last-child').first()
|
||||
expect(await lineCount(lastTd)).toBe(normalLines - 1)
|
||||
expect(await visibleLineCount(lastTd)).toBe(normalLines)
|
||||
|
||||
// 横线**不能**是真 border:格线一进盒模型,单元格多 1px 就会被行内编辑器顶高
|
||||
// (行高是虚拟滚动 translateY 的基准)。
|
||||
expect(await td.evaluate((el) => getComputedStyle(el).borderBottomWidth)).toBe('0px')
|
||||
expect(await td.evaluate((el) => getComputedStyle(el).borderRightWidth)).toBe('0px')
|
||||
|
||||
// 行高没被格线动过(30 = 默认 rowHeight,也是虚拟滚动 translateY 的基准)
|
||||
expect(
|
||||
@@ -675,24 +809,47 @@ test.describe('G3Table 布局:表头固定与分隔线', () => {
|
||||
).toBe(30)
|
||||
})
|
||||
|
||||
test('库自绘边框:单元格带可见的横竖渐变线', async ({ page }) => {
|
||||
test('纵向格线:表体 td 无竖线,表头 th 的列界不占满高度且颜色更轻', 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)
|
||||
// ── 表体:一条竖线都没有(全局涂透明),只剩横线那一条。
|
||||
// 读「变量」是因为变量就是库的输入、语义最直接,不会随库内部怎么拼 image 而失效。
|
||||
// ⚠ 断言里**不能**出现 `var(--border-color)` 这类字面量:自定义属性的计算值会把
|
||||
// 里面的 var() 全部替换掉(规范如此),拿到的已经是 `#d9d9d9`。
|
||||
const td = wrap.locator('.stk-tbody-main td').first()
|
||||
const tdVars = await td.evaluate((el) => {
|
||||
const cs = getComputedStyle(el)
|
||||
return {
|
||||
right: cs.getPropertyValue('--bg-border-right').trim(),
|
||||
left: cs.getPropertyValue('--bg-border-left').trim(),
|
||||
}
|
||||
})
|
||||
expect(tdVars.right).toContain('transparent')
|
||||
expect(tdVars.left).toContain('transparent')
|
||||
expect(tdVars.right).not.toMatch(/rgb\(|#[0-9a-f]{3}/i)
|
||||
expect(await visibleLineCount(td)).toBe(1)
|
||||
|
||||
// ── 表头:列界由组件自己接管 background-image 画(不是库的 --bg-border-right),
|
||||
// 两层可见线 = 短竖线 + 横线。
|
||||
const th = wrap.locator('thead th').first()
|
||||
const thPaint = await th.evaluate((el) => {
|
||||
const cs = getComputedStyle(el)
|
||||
return {
|
||||
image: cs.backgroundImage,
|
||||
size: cs.backgroundSize,
|
||||
position: cs.backgroundPosition,
|
||||
}
|
||||
})
|
||||
expect(await visibleLineCount(th)).toBe(2)
|
||||
|
||||
// 不占满:竖线那层的高度是 calc(100% - 8px) —— 上下各留 4px 的呼吸位
|
||||
expect(thPaint.size.split(',')[0].trim()).toBe('1px calc(100% - 8px)')
|
||||
expect(thPaint.position.split(',')[0].trim()).toBe('100% 4px')
|
||||
|
||||
// 更轻:竖线是 8% 的黑(半透明),不是格线那种不透明色
|
||||
expect(thPaint.image).toMatch(/rgba\(0,\s*0,\s*0,\s*0\.08\)/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1153,6 +1310,284 @@ test.describe('G3Table 键盘跳格:Tab / Shift+Tab', () => {
|
||||
* 两个宿主覆盖两条 CSS 分支:`table-stripe-wrap`(2 行,纵向虚拟关闭 → `nth-child(even)`)
|
||||
* 与 `table-big-wrap`(5000 行 → `vt-on`,库改取 `nth-child(odd)`)。
|
||||
*/
|
||||
/**
|
||||
* 日期编辑器(`editor.type = 'date'`)。
|
||||
*
|
||||
* 宿主是 playground 的 `table-date-wrap` —— 单独一张小表:`table-wrap` 的列序被 Tab
|
||||
* 跳格用例逐格锁着(amount → status → enabled → remark 的顺位),往它里面加列会连带
|
||||
* 改一串断言。
|
||||
*
|
||||
* 列:订舱号(只读) / 开航日(date,默认 `YYYY-MM-DD`,配了必填校验) /
|
||||
* 截单日(date,`'YYYY-MM-DD HH:mm'`,用来覆盖「带时间精度」的配置形态)。
|
||||
*/
|
||||
/**
|
||||
* 方向键的分工(编辑器 vs 表格)。
|
||||
*
|
||||
* 本组件的交互是**单击即进入行编辑**(点一格就能直接打字,少一次点击)。在这个前提下,
|
||||
* 方向键必须显式分工 —— 否则会全部被输入框吃掉,人在表格里根本走不动:
|
||||
*
|
||||
* · **左右** → 交给输入框移动光标 / `Shift+左右` 选中文本(「想改中间一位」的刚需);
|
||||
* · **上下** → 让给表格导航,切到上 / 下一行的同一列
|
||||
* (单行输入框里上下键**没有任何原生用途**,让出去是零损失);
|
||||
* · `Shift+上下` → 仍归输入框(原生语义是「选中到首尾」);
|
||||
* · `textarea` 里的上下 → 也归输入框(要跨行移动光标)。
|
||||
*
|
||||
* 宿主是 playground 的 `table-keys-wrap`:text / number / textarea 三列。
|
||||
*/
|
||||
test.describe('G3Table 方向键分工:左右归输入框、上下给导航', () => {
|
||||
test('单击即进入编辑:挂上编辑器并自动聚焦', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-keys-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const cell = wrap.locator('.stk-tbody-main tr').first().locator('td[data-col-key="code"]')
|
||||
|
||||
await cell.click()
|
||||
await expect(cell.locator('input')).toHaveValue('K-001')
|
||||
await expect(cell.locator('input')).toBeFocused()
|
||||
})
|
||||
|
||||
test('上下键:切到上 / 下一行的同一列(并且进编辑,可以直接接着改)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-keys-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const rows = wrap.locator('.stk-tbody-main tr')
|
||||
const first = rows.nth(0).locator('td[data-col-key="code"]')
|
||||
|
||||
await first.click()
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await expect(rows.nth(1).locator('td[data-col-key="code"] input')).toBeFocused()
|
||||
|
||||
// 反向同样有效(连续按能一直在这一列上下走,手不离开键盘)
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await expect(first.locator('input')).toBeFocused()
|
||||
})
|
||||
|
||||
test('左右键:在输入框内移动光标,不切格', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-keys-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const cell = wrap.locator('.stk-tbody-main tr').first().locator('td[data-col-key="qty"]')
|
||||
|
||||
await cell.click()
|
||||
const input = cell.locator('input')
|
||||
await input.fill('12345')
|
||||
|
||||
// 不假设 fill 之后光标落在哪,只断言「按一次左键,光标左移一位」
|
||||
const before = await input.evaluate((el) => (el as HTMLInputElement).selectionStart)
|
||||
await input.press('ArrowLeft')
|
||||
const after = await input.evaluate((el) => (el as HTMLInputElement).selectionStart)
|
||||
|
||||
expect(after).toBe((before ?? 0) - 1)
|
||||
await expect(input).toBeFocused()
|
||||
// 还在同一格:左右键没有冒到表格导航上
|
||||
await expect(cell.locator('input')).toHaveCount(1)
|
||||
})
|
||||
|
||||
test('Shift+左右:留给输入框选中文本,不切格', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-keys-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const cell = wrap.locator('.stk-tbody-main tr').first().locator('td[data-col-key="code"]')
|
||||
|
||||
await cell.click()
|
||||
const input = cell.locator('input')
|
||||
// 进入编辑时编辑器会**全选**内容(为「直接输入即替换」服务),所以先记下初始选区长度
|
||||
const before = await input.evaluate((el) => {
|
||||
const node = el as HTMLInputElement
|
||||
return (node.selectionEnd ?? 0) - (node.selectionStart ?? 0)
|
||||
})
|
||||
await input.press('Shift+ArrowLeft')
|
||||
await input.press('Shift+ArrowLeft')
|
||||
|
||||
// 结果是「选区缩小了两个字符」,而不是「跳到左边那一格」
|
||||
const after = await input.evaluate((el) => {
|
||||
const node = el as HTMLInputElement
|
||||
return (node.selectionEnd ?? 0) - (node.selectionStart ?? 0)
|
||||
})
|
||||
expect(after).toBe(before - 2)
|
||||
await expect(input).toBeFocused()
|
||||
})
|
||||
|
||||
test('textarea:上下键留给它跨行移动光标,不切格', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-keys-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const cell = wrap.locator('.stk-tbody-main tr').first().locator('td[data-col-key="note"]')
|
||||
|
||||
await cell.click()
|
||||
const textarea = cell.locator('textarea')
|
||||
await textarea.fill('第一行')
|
||||
await textarea.press('Enter')
|
||||
await textarea.type('第二行')
|
||||
|
||||
// 光标在第二行 → 按上键应当回到第一行(列号保持),而不是跳到上一行的格子里
|
||||
await textarea.press('ArrowUp')
|
||||
await expect(textarea).toBeFocused()
|
||||
const caret = await textarea.evaluate((el) => (el as HTMLTextAreaElement).selectionStart)
|
||||
// "第一行\n第二行":第一行末尾是索引 3("第一行" 三个字)
|
||||
expect(caret).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 键盘行为:Enter 与多行编辑器。
|
||||
*
|
||||
* · Enter(单行)→ 确认 + 下移到下一行同列(Excel / Sheets / AG Grid / Handsontable 的通行约定);
|
||||
* · Enter(多行)→ 换行(浏览器原生;多行与单行共用一段 onKeydown,早先没按类型分流,
|
||||
* 按回车会把整段文本全选掉、根本换不了行)。
|
||||
*
|
||||
* 方向键的分工(左右归输入框、上下给导航)在另一个 describe 里;宿主相同 ——
|
||||
* `table-wrap` 的列序被 Tab 跳格用例逐格锁着,而且它里面没有多行编辑器,
|
||||
* 锁不住「单行 / 多行 Enter 语义不同」这条区分。
|
||||
*/
|
||||
test.describe('G3Table 键盘行为:Enter 与多行编辑器', () => {
|
||||
test('Enter(单行):确认并下移到下一行的同一列', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-keys-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const rows = wrap.locator('.stk-tbody-main tr')
|
||||
const firstCell = rows.nth(0).locator('td[data-col-key="code"]')
|
||||
|
||||
await firstCell.click()
|
||||
await firstCell.locator('input').fill('K-999')
|
||||
await firstCell.locator('input').press('Enter')
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// 值**已提交**:旧行为是「全选文本、不提交」,改完按回车值还没落库。
|
||||
// 断言整格文本而不是 `.cell-display` —— 这一列没配 validate/precision,
|
||||
// 非编辑态走的是库的原生 td 渲染(连我们那个 span 都没有),这是有意为之的零开销路径。
|
||||
await expect(firstCell).toHaveText('K-999')
|
||||
// 编辑格落到下一行的**同一列**,且焦点已经在它里面(走的是改选区那条链路)
|
||||
await expect(rows.nth(1).locator('td[data-col-key="code"] input')).toBeFocused()
|
||||
})
|
||||
|
||||
test('Enter(多行):是换行,不是下移', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-keys-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const cell = wrap.locator('.stk-tbody-main tr').first().locator('td[data-col-key="note"]')
|
||||
|
||||
await cell.click()
|
||||
const textarea = cell.locator('textarea')
|
||||
await textarea.fill('第一行')
|
||||
await textarea.press('Enter')
|
||||
await textarea.type('第二行')
|
||||
|
||||
// 换行符真的进去了,而且**还停在同一格**(没有跳到下一行)
|
||||
await expect(textarea).toHaveValue('第一行\n第二行')
|
||||
await expect(textarea).toBeFocused()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
test.describe('G3Table 日期编辑器(editor.type = date)', () => {
|
||||
test('点日期格即弹日历,选中的日期写回字符串', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-date-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const cell = wrap.locator('.stk-tbody-main tr').first().locator('td[data-col-key="etd"]')
|
||||
|
||||
// 展示态就是存进去的字符串,组件不做任何格式转换
|
||||
await expect(cell.locator('.g3-table__cell-display')).toHaveText('2026-03-15')
|
||||
|
||||
/**
|
||||
* ① 点格 → 自动聚焦 **且自动弹开日历面板**。
|
||||
*
|
||||
* 这是 `type: 'date'` 的核心交互:点格的那次点击落在 `td` 上,触发不到 DatePicker
|
||||
* 自己的「点输入框才展开」,所以要由挂载钩子主动调它的 `openPanel()`
|
||||
* (见 `utils/editable.ts` 的 openDatePanel)。手感与 ERP 里的日期格一致。
|
||||
*/
|
||||
await cell.click()
|
||||
/**
|
||||
* 焦点**落在面板里**(CalendarPanel 的日期格是 `<button>` 且带 tabindex),
|
||||
* 这是日历组件的常规行为:一打开就能用键盘直接选日期。
|
||||
* 所以这里不能断言「输入框持有焦点」—— 想手输要先点一下输入框(见下一条用例)。
|
||||
*/
|
||||
await expect(cell.locator('input')).toHaveCount(1)
|
||||
const panel = page.locator('.g3-calendar')
|
||||
await expect(panel).toBeVisible()
|
||||
|
||||
/**
|
||||
* ② 面板里选一天 → 按 `format`(没配 valueFormat 时跟随它)写回字符串。
|
||||
*
|
||||
* 面板打开时定位在**当前值所在月份**,所以这一格能稳定点到同月的某一天;
|
||||
* 空值格会落在「今天」那个月,不适合写死日期断言(那是另一条用例的事)。
|
||||
*/
|
||||
await panel.locator('.g3-calendar__cell[data-key="2026-03-20"]').click()
|
||||
// 选完面板自动关闭;这一格**仍在编辑态**(选值不退出编辑),所以断言输入框的值
|
||||
await expect(panel).toBeHidden()
|
||||
await expect(cell.locator('input')).toHaveValue('2026-03-20')
|
||||
|
||||
// 切到别的格退出编辑 → 展示态显示的是刚写回行数据的字符串(证明值真的落了库)
|
||||
await wrap.locator('.stk-tbody-main tr').nth(1).locator('td[data-col-key="etd"]').click()
|
||||
await expect(cell.locator('.g3-table__cell-display')).toHaveText('2026-03-20')
|
||||
})
|
||||
|
||||
test('必填校验与手输提交:空值是 error,手输失焦后写回', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-date-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const rows = wrap.locator('.stk-tbody-main tr')
|
||||
const empty = rows.nth(1).locator('td[data-col-key="etd"]')
|
||||
|
||||
// 第 2 行开航日是空串 → 列上配的 validate 判它必填,进页面就是 error
|
||||
await expect(empty.locator('.is-editor-error')).toHaveCount(1)
|
||||
|
||||
// 悬停那格能看到原因:日期列同样接上了统一的那套失败原因浮层
|
||||
await empty.hover()
|
||||
await expect(page.locator('.g3-table__cell-tip')).toHaveText('请选择开航日')
|
||||
|
||||
// 手输一个合法日期,再点**另一行的日期格**让它失焦提交(DatePicker 只在「确定的时刻」emit)
|
||||
await empty.click()
|
||||
await empty.locator('input').fill('2026-05-01')
|
||||
/**
|
||||
* ⚠️ 这里必须点**可编辑格**。点只读列(订舱号)不会让编辑器失焦 ——
|
||||
* 库在 mousedown 阶段就把只读格的事件吞了,input 一直持有焦点,
|
||||
* 表现是「输了值但没落库」(实测踩到)。
|
||||
*/
|
||||
await rows.nth(2).locator('td[data-col-key="etd"]').click()
|
||||
await page.waitForTimeout(250)
|
||||
|
||||
await expect(empty.locator('.g3-table__cell-display')).toHaveText('2026-05-01')
|
||||
// 值补上了,error 自然消失(校验是行数据的派生值)
|
||||
await expect(empty.locator('.is-editor-error')).toHaveCount(0)
|
||||
})
|
||||
|
||||
/**
|
||||
* 手输非法日期。
|
||||
*
|
||||
* 这条锁的是「**值不丢弃**」这个决定:DatePicker 有意不为解析不出来的文本 emit
|
||||
* (否则表单的 `v-model` 会被 `2026-13-45` 污染),表格侧在编辑器卸载时把它
|
||||
* 原样写回行数据,再交给内置的日期校验标红 —— 与「金额输 0」是同一套立场:
|
||||
* 值不合法不等于要抹掉它,而是要让人看见、并知道改成什么。
|
||||
*/
|
||||
test('手输非法日期:值原样保留、标红,悬停能看到原因', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const wrap = page.locator('[data-testid="table-date-wrap"]')
|
||||
await wrap.scrollIntoViewIfNeeded()
|
||||
const rows = wrap.locator('.stk-tbody-main tr')
|
||||
const cell = rows.first().locator('td[data-col-key="etd"]')
|
||||
|
||||
await cell.click()
|
||||
await cell.locator('input').fill('2026-13-45')
|
||||
await rows.nth(2).locator('td[data-col-key="etd"]').click()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// 上一步点第 3 行会顺手弹出它自己的日历面板 —— 那是 Teleport 到 body 的浮层,
|
||||
// 会盖住下面要 hover 的第一行。先按 Esc 收掉(只影响刚点开的那个面板)。
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// 值原样保留(不是被丢弃成空、也不是回退成旧值),并进入 error 态
|
||||
await expect(cell.locator('.g3-table__cell-display')).toHaveText('2026-13-45')
|
||||
await expect(cell.locator('.is-editor-error')).toHaveCount(1)
|
||||
|
||||
// 原因走的是统一的那套浮层,日期列没有另做提示
|
||||
await cell.hover()
|
||||
await expect(page.locator('.g3-table__cell-tip')).toHaveText('日期格式不正确')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('G3Table 斑马纹与固定列', () => {
|
||||
/**
|
||||
* 取某一行**第一个单元格**(固定列/序号列)的底色与叠色情况。
|
||||
|
||||
Reference in new issue
Block a user