This commit is contained in:
oneao committed 2026-10-12 22:25:41 +08:00
1 parent ae3ca3cfa1
commit 7bd5f1dde9
45 files changed
+3414 -229

No files matched your search

@@ -0,0 +1,60 @@
import { expect, test } from '@playwright/test'
/**
* 验证去掉分页、默认 1000 行后的 /ocean/booking。
*
* 注意:ERP 的 Vite dep 缓存可能是旧包(曾实测缓存里没有 G3Table),
* 若行编辑相关断言失败,先确认缓存已刷新再判断是页面问题。
*/
test.describe('/ocean/booking(无分页,1000 行)', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/ocean/booking')
await page.waitForTimeout(1500)
})
test('页面渲染表格,且分页器已移除', async ({ page }) => {
await expect(page.locator('.booking__title')).toHaveText('订舱单')
await expect(page.locator('.g3-table')).toBeVisible()
// 分页器不该再存在
await expect(page.locator('.g3-pagination')).toHaveCount(0)
// 「每页」选择器也不该存在
await expect(page.locator('.booking__field').filter({ hasText: '每页' })).toHaveCount(0)
})
test('数据量选择器默认 1000,且只保留一个下拉', async ({ page }) => {
const selects = page.locator('.booking__select')
await expect(selects).toHaveCount(1)
await expect(selects.first()).toHaveValue('1000')
})
test('1000 行数据:虚拟滚动开启,DOM 行数远少于 1000', async ({ page }) => {
const stk = page.locator('.g3-table .stk-table').first()
// 库开启虚拟滚动时会加 .virtual 类
await expect(stk).toHaveClass(/virtual/)
const rendered = await page.locator('.stk-tbody-main tr').count()
console.log('DOM ROWS (1000 数据): ' + rendered)
expect(rendered).toBeGreaterThan(0)
expect(rendered).toBeLessThan(200)
})
test('可滚动到底部(数据确实有 1000 行)', async ({ page }) => {
const info = await page.locator('.g3-table .stk-table').first().evaluate((el) => {
const stk = el as HTMLElement
return { scrollH: stk.scrollHeight, clientH: stk.clientHeight }
})
console.log('SCROLL: ' + JSON.stringify(info))
// 1000 行 * 32px 行高 ≈ 32000,远大于视口
expect(info.scrollH).toBeGreaterThan(10000)
})
test('切换数据量到 100 行后虚拟滚动关闭(低于阈值)', async ({ page }) => {
await page.locator('.booking__select').first().selectOption('100')
await page.waitForTimeout(600)
const stk = page.locator('.g3-table .stk-table').first()
await expect(stk).not.toHaveClass(/virtual/)
// 100 行全量渲染
await expect(page.locator('.stk-tbody-main tr')).toHaveCount(100)
})
})
@@ -0,0 +1,87 @@
import { expect, test } from '@playwright/test'
/**
* 复现用户报的问题:
* 编辑态里的 Select,点选项后**不立即**生效;要点别处才更新。
* 「select 消失了,但框里的数据没变」。
*
* 严格控制步骤,每步都记录 DOM 实际内容与 row 数据。
*/
test('reproduce: select value not applied immediately', async ({ page }) => {
const logs: string[] = []
page.on('pageerror', (e) => logs.push('pageerror: ' + e.message))
page.on('console', (m) => {
if (m.type() === 'error' || m.type() === 'warning') logs.push('[' + m.type() + '] ' + m.text().slice(0, 200))
})
await page.goto('/ocean/booking')
await page.waitForTimeout(2000)
const rowState = () =>
page.evaluate(() => {
const root = document.querySelector('.g3-table') as HTMLElement
const rows = Array.from(root.querySelectorAll('.stk-tbody-main tr')).filter(
(r) => !(r as HTMLElement).className.includes('padding-top'),
) as HTMLElement[]
const tr = rows[0]
// 起运港列(pol)—— 用 data-col-key 精确定位
const polTd = tr?.querySelector('td[data-col-key="pol"]') as HTMLElement | null
const editor = polTd?.querySelector('.g3-select') as HTMLElement | null
const displayText = polTd?.textContent?.trim() ?? null
// 触发器里显示的文字
const triggerInput = editor?.querySelector('input') as HTMLInputElement | null
return {
rowKey: tr?.getAttribute('data-row-key'),
polTdText: displayText,
hasSelectEditor: !!editor,
triggerValue: triggerInput?.value ?? null,
// 展示态(非编辑)的文本
displaySpan: polTd?.querySelector('.g3-table__cell-display')?.textContent ?? null,
}
})
console.log('1) 初始: ' + JSON.stringify(await rowState()))
// 点起运港单元格进入编辑
const polCell = page.locator('.stk-tbody-main tr:not(.padding-top-tr)').first().locator('td[data-col-key="pol"]')
await polCell.click()
await page.waitForTimeout(600)
console.log('2) 进入编辑后: ' + JSON.stringify(await rowState()))
// 打开下拉
const selectRoot = polCell.locator('.g3-select')
await selectRoot.click()
await page.waitForTimeout(500)
const dropdownInfo = await page.evaluate(() => {
const panels = Array.from(document.querySelectorAll('.g3-select__dropdown, [role="listbox"], .g3-popover'))
return {
panelCount: panels.length,
options: panels.length
? Array.from(panels[panels.length - 1].querySelectorAll('[role="option"], li, .g3-select__option')).map((o) =>
(o.textContent || '').trim().slice(0, 12),
)
: [],
}
})
console.log('3) 下拉内容: ' + JSON.stringify(dropdownInfo))
// 点「宁波」
const option = page.locator('[role="option"], .g3-select__option').filter({ hasText: '宁波' }).first()
const optCount = await option.count()
console.log('4) 找到「宁波」选项数: ' + optCount)
if (optCount > 0) {
await option.click()
await page.waitForTimeout(700)
console.log('5) 点完选项后: ' + JSON.stringify(await rowState()))
console.log(' (期望 pol 立即变成 CNNGB / 宁波)')
// 再点空白处
await page.locator('.booking__title').click()
await page.waitForTimeout(700)
console.log('6) 点空白处后: ' + JSON.stringify(await rowState()))
}
console.log('ERRORS: ' + JSON.stringify(logs.slice(0, 8), null, 2))
expect(true).toBe(true)
})
@@ -0,0 +1,202 @@
import { expect, test } from '@playwright/test'
/**
* /ocean/booking 行编辑验证(修正版)。
*
* 上一版断言写错了:按 Enter 后期待 change 回写 —— 但 Enter 的语义是
* 「全选当前内容」(便于直接覆写),落库靠 **失焦**。见 utils/editable.ts:
* if (key === 'Enter') { preventDefault(); stopPropagation(); target.select() }
* onBlur: commitInputDraft(...) + notifyChange(...)
* 所以验证回写必须用 blur(点别处 / Tab 切走),不能只按 Enter。
*/
test.describe('/ocean/booking 行编辑', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/ocean/booking')
await page.waitForTimeout(2000)
})
test('区域选择已启用(注册时机修复的正面证据)', async ({ page }) => {
const cls = await page.evaluate(
() => (document.querySelector('.g3-table .stk-table') as HTMLElement)?.className ?? '',
)
console.log('STK CLASSES: ' + cls)
expect(cls).toContain('area-selection')
})
test('单击单元格进入编辑,焦点落在该格', async ({ page }) => {
const cell = page
.locator('.stk-tbody-main tr:not(.padding-top-tr)')
.first()
.locator('td')
.nth(2)
const before = await cell.innerText()
console.log('CELL BEFORE: ' + before)
await cell.click()
await page.waitForTimeout(600)
// 编辑器出现
await expect(page.locator('.g3-table__cell-editor-root').first()).toBeVisible()
// 焦点在该格的 input 上,且带出原值
const focused = await page.evaluate(() => {
const a = document.activeElement as HTMLInputElement | null
return { tag: a?.tagName, value: a?.value ?? null }
})
console.log('FOCUSED: ' + JSON.stringify(focused))
expect(focused.tag).toBe('INPUT')
expect(focused.value).toBe(before)
})
test('输入后失焦落库并抛出 change', async ({ page }) => {
const cell = page
.locator('.stk-tbody-main tr:not(.padding-top-tr)')
.first()
.locator('td')
.nth(2)
await cell.click()
await page.waitForTimeout(500)
await page.keyboard.press('Control+A')
await page.keyboard.type('测试客户A')
await page.waitForTimeout(200)
// 关键:用失焦提交(不是 Enter —— Enter 的语义是"全选以便覆写",见下一条用例)
await page.locator('.booking__title').click()
await page.waitForTimeout(700)
const tip = await page.locator('.booking__tip').innerText()
console.log('TIP: ' + tip.replace(/\n/g, ' | '))
expect(tip).toContain('测试客户A')
// 校验新值已落到单元格上。
//
// 试过三种写法,前两种都是错的,记在这里免得再踩:
// 1) cell.innerText() —— 编辑态里是 <input>,innerText 取不到 value,得空串
// 2) 断言 input 数量为 0 —— 错。editingActive 一旦为 true,整行可编辑列都保留
// 编辑器(性能设计),input 一直在,不等于"还在编辑"
// 3) .g3-table__cell-display —— 也错。失焦后该行仍处编辑态(编辑器未卸载),
// 展示态节点根本不存在
//
// 真正可靠的做法:直接读编辑器 input 的 value(它就是该格当前值的真源),
// 并用「焦点是否还在该格」判断编辑是否结束。
const inputValue = await cell.locator('input').first().inputValue()
console.log('CELL INPUT VALUE AFTER BLUR: ' + inputValue)
expect(inputValue).toBe('测试客户A')
const stillFocused = await page.evaluate(() => {
const td = document.activeElement?.closest('td')
return !!td?.querySelector('.g3-table__cell-editor-root')
})
console.log('STILL FOCUSED IN EDITOR: ' + stillFocused)
expect(stillFocused).toBe(false)
})
test('Enter 的语义是全选而非提交', async ({ page }) => {
const cell = page
.locator('.stk-tbody-main tr:not(.padding-top-tr)')
.first()
.locator('td')
.nth(2)
await cell.click()
await page.waitForTimeout(500)
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
// 全选:input 的 selectionStart/End 覆盖整个值
const sel = await page.evaluate(() => {
const a = document.activeElement as HTMLInputElement | null
return { len: a?.value.length ?? 0, s: a?.selectionStart ?? -1, e: a?.selectionEnd ?? -1 }
})
console.log('SELECTION AFTER ENTER: ' + JSON.stringify(sel))
expect(sel.s).toBe(0)
expect(sel.e).toBe(sel.len)
expect(sel.len).toBeGreaterThan(0)
})
/**
* Esc = **取消编辑并恢复原值**。
*
* 与 Excel / AG Grid / Handsontable / antd 的通用约定一致(Enter 提交、Esc 取消)。
*
* 改前的错误行为:Esc 只是 blur,而 blur 会走 onBlur → commit + notifyChange,
* 等于「按 Esc 反而保存了」(从参考实现 fms 原样照抄而来)。
* 现在 utils/editable.ts 的 Escape 分支会先 ctx.revertRow(row) 再 blur ——
* 回滚后 input 里是原值,随后的 blur 提交的就是原值,change 被去重挡掉。
*/
test('Esc 取消编辑并恢复原值', async ({ page }) => {
const cell = page
.locator('.stk-tbody-main tr:not(.padding-top-tr)')
.first()
.locator('td')
.nth(2)
const original = (await cell.innerText()).trim()
console.log('ORIGINAL VALUE: ' + original)
await cell.click()
await page.waitForTimeout(500)
await page.keyboard.press('Control+A')
await page.keyboard.type('不该保存')
await page.waitForTimeout(200)
// 输入过程中不该有 change(草稿机制)
const tipDuringTyping = await page.locator('.booking__tip').innerText()
expect(tipDuringTyping).not.toContain('不该保存')
await page.keyboard.press('Escape')
await page.waitForTimeout(700)
const tip = await page.locator('.booking__tip').innerText()
console.log('TIP AFTER ESC: ' + tip.replace(/\n/g, ' | '))
// 核心:Esc 不产生 change(值已回滚,被去重挡掉)
expect(tip).not.toContain('不该保存')
// 编辑器里的值应已恢复成原值
const valueAfterEsc = await cell.locator('input').first().inputValue()
console.log('INPUT VALUE AFTER ESC: ' + valueAfterEsc)
expect(valueAfterEsc).toBe(original)
// 焦点应已离开编辑器
const focusedInCell = await page.evaluate(() => {
const td = document.activeElement?.closest('td')
return !!td?.querySelector('.g3-table__cell-editor-root')
})
console.log('STILL FOCUSED IN EDITOR: ' + focusedInCell)
expect(focusedInCell).toBe(false)
})
test('Esc 回滚后仍可正常再次编辑并提交', async ({ page }) => {
const cell = page
.locator('.stk-tbody-main tr:not(.padding-top-tr)')
.first()
.locator('td')
.nth(2)
// 第一次:改后 Esc 回滚
await cell.click()
await page.waitForTimeout(400)
await page.keyboard.press('Control+A')
await page.keyboard.type('第一次改动')
await page.keyboard.press('Escape')
await page.waitForTimeout(600)
expect(await cell.locator('input').first().inputValue()).not.toBe('第一次改动')
// 第二次:正常提交应仍然生效(回滚不能把去重基线弄坏)
await cell.click()
await page.waitForTimeout(400)
await page.keyboard.press('Control+A')
await page.keyboard.type('第二次改动')
await page.locator('.booking__title').click()
await page.waitForTimeout(700)
const tip = await page.locator('.booking__tip').innerText()
console.log('TIP AFTER SECOND COMMIT: ' + tip.replace(/\n/g, ' | '))
expect(tip).toContain('第二次改动')
})
})
@@ -0,0 +1,65 @@
import { expect, test } from '@playwright/test'
/**
* 验证行高修复:编辑前后行高必须都等于 rowHeight(32px)。
* 这是当初就该有、却一直缺失的断言 —— 组件里有防此问题的意图(height: var(--row-height)),
* 但从没有任何测试验证它真的生效。
*/
test('编辑前后行高保持 32px', async ({ page }) => {
await page.goto('/ocean/booking')
await page.waitForTimeout(2000)
const measure = () =>
page.evaluate(() => {
const root = document.querySelector('.g3-table') as HTMLElement
const rows = Array.from(root.querySelectorAll('.stk-tbody-main tr')).filter(
(r) => !(r as HTMLElement).className.includes('padding-top'),
) as HTMLElement[]
const h = (el?: HTMLElement | null) =>
el ? Math.round(el.getBoundingClientRect().height * 100) / 100 : null
const editing = rows[0]
const tdEditor = editing?.querySelector(
'td:has(> .g3-table__cell-editor-root)',
) as HTMLElement | null
const er = tdEditor?.firstElementChild as HTMLElement | null
return {
rowHeights: rows.slice(0, 4).map((r) => h(r)),
editorTd: h(tdEditor),
editorRoot: h(er),
editorTdPad: tdEditor ? getComputedStyle(tdEditor).padding : null,
rowHeightVar: getComputedStyle(root.querySelector('.stk-table') as HTMLElement)
.getPropertyValue('--row-height')
.trim(),
}
})
const before = await measure()
console.log('BEFORE: ' + JSON.stringify(before))
const cell = page.locator('.stk-tbody-main tr:not(.padding-top-tr)').first().locator('td').nth(2)
await cell.click()
await page.waitForTimeout(700)
const during = await measure()
console.log('DURING: ' + JSON.stringify(during))
// 退出编辑
await page.keyboard.press('Escape')
await page.waitForTimeout(600)
const after = await measure()
console.log('AFTER: ' + JSON.stringify(after))
// 核心断言:三态行高一致,且等于 --row-height
expect(before.rowHeights[0]).toBe(32)
expect(during.rowHeights[0]).toBe(32) // ← 修复前是 42
expect(after.rowHeights[0]).toBe(32)
// 同行其它行不受影响
expect(during.rowHeights[1]).toBe(32)
// 编辑器确实存在(确认真的进入了编辑态,而不是断言空过)
const editorCount = await page.evaluate(
() => document.querySelectorAll('.g3-table__cell-editor-root').length,
)
console.log('EDITOR COUNT (退出后应为 0): ' + editorCount)
})