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() —— 编辑态里是 ,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('第二次改动') }) })