u
This commit is contained in:
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)
|
||||
})
|
||||
Reference in new issue
Block a user