u
This commit is contained in:
1 parent
e7c84a82a2
commit
025d3232c4
65 files changed
+9838
-9111
No files matched your search
@@ -0,0 +1,450 @@
|
||||
import { chromium } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* /table-test 性能探针 v5 —— 归因(只读诊断,不改仓库代码)。
|
||||
*
|
||||
* 已确认事实(v3/v4):
|
||||
* · 静止 61 FPS;纵向滚动 ~11 FPS;横向滚动 ~10 FPS;编辑态纵向 ~8 FPS
|
||||
* · 点击格 → 编辑器就绪:中位 139ms,p95 444ms
|
||||
* · 去掉全部 editor(零 customCell)后纵向仍只有 17 FPS → 底座自身也有地板
|
||||
* · 1000 行与 5000 行帧率几乎相同 → 与总行数无关(O(可见格))
|
||||
*
|
||||
* 本版要回答:
|
||||
* Q1 每帧的 74~90ms 里,JS(Script) 与 样式/布局(Style/Layout/Paint) 各占多少?→ CDP Performance 指标
|
||||
* Q2 横向滚动时到底有没有发生「本不该发生的」DOM 变更?→ MutationObserver 计数
|
||||
* Q3 CSS 层里哪一条最贵?→ 注入 !important 覆盖做 A/B(背景渐变 / box-shadow / sticky)
|
||||
* Q4 点击一次编辑,钱花在哪几步?→ 分阶段打点
|
||||
* Q5 页面 errorCount(1000 行 × 21 列)重算一次要多久?
|
||||
*/
|
||||
|
||||
const BASE = process.env.PROBE_URL || 'http://127.0.0.1:5090'
|
||||
const MENU_CODE = 'menu.table_test'
|
||||
|
||||
const INST = `(() => {
|
||||
let el = document.querySelector('.stk-table')
|
||||
let inst = el && (el.__vueParentComponent || null)
|
||||
return inst || null
|
||||
})()`
|
||||
|
||||
async function makePage() {
|
||||
const browser = await chromium.launch()
|
||||
const context = await browser.newContext({ viewport: { width: 1600, height: 900 } })
|
||||
await context.route('**/api/data/loaddata', async (route) => {
|
||||
let viewName = ''
|
||||
try {
|
||||
viewName = JSON.parse(route.request().postData() || '').view_name || ''
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const response = await route.fetch()
|
||||
if (viewName !== 's_power' && viewName !== 's_user_power') return route.fulfill({ response })
|
||||
let json
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch {
|
||||
return route.fulfill({ response })
|
||||
}
|
||||
const rows = Array.isArray(json?.data) ? json.data : []
|
||||
if (viewName === 's_power') {
|
||||
if (!rows.some((r) => r.b_id === MENU_CODE)) rows.push({ b_id: MENU_CODE, b_canuse: 1 })
|
||||
} else if (!rows.some((r) => r.b_power_id === MENU_CODE)) {
|
||||
rows.push({ b_user_id: 'demo_zhangwei', b_power_id: MENU_CODE, b_canuse: 1 })
|
||||
}
|
||||
json.data = rows
|
||||
await route.fulfill({
|
||||
response,
|
||||
body: JSON.stringify(json),
|
||||
headers: { ...response.headers(), 'content-type': 'application/json' },
|
||||
})
|
||||
})
|
||||
const page = await context.newPage()
|
||||
page.on('pageerror', (e) => console.log('[pageerror]', String(e).slice(0, 200)))
|
||||
await page.goto(`${BASE}/table-test`, { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForSelector('.stk-table', { timeout: 90000 })
|
||||
await page.waitForTimeout(2500)
|
||||
return { browser, context, page }
|
||||
}
|
||||
|
||||
/** CDP Performance 指标:分离 JS 与 样式/布局 */
|
||||
async function metricsDuring(page, context, fn) {
|
||||
const client = await context.newCDPSession(page)
|
||||
await client.send('Performance.enable')
|
||||
const pick = (m) => {
|
||||
const o = {}
|
||||
for (const x of m.metrics) o[x.name] = x.value
|
||||
return o
|
||||
}
|
||||
const before = pick(await client.send('Performance.getMetrics'))
|
||||
await fn()
|
||||
const after = pick(await client.send('Performance.getMetrics'))
|
||||
await client.detach()
|
||||
const d = (k) => +((after[k] ?? 0) - (before[k] ?? 0)).toFixed(3)
|
||||
return {
|
||||
TaskDuration: d('TaskDuration'),
|
||||
ScriptDuration: d('ScriptDuration'),
|
||||
StyleDuration: d('RecalcStyleDuration'),
|
||||
LayoutDuration: d('LayoutDuration'),
|
||||
RecalcStyleCount: d('RecalcStyleCount'),
|
||||
LayoutCount: d('LayoutCount'),
|
||||
Nodes: d('Nodes'),
|
||||
JSHeapMB: +((after.JSHeapUsedSize ?? 0) / 1048576).toFixed(1),
|
||||
}
|
||||
}
|
||||
|
||||
/** 滚动(真实驱动 .stk-table) */
|
||||
async function doScroll(page, { axis = 'y', steps = 120, px = 60 } = {}) {
|
||||
return page.evaluate(
|
||||
async ({ axis, steps, px }) => {
|
||||
const cont = document.querySelector('.stk-table')
|
||||
for (let i = 0; i < steps; i += 1) {
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
if (axis === 'y') {
|
||||
const max = cont.scrollHeight - cont.clientHeight
|
||||
cont.scrollTop = cont.scrollTop >= max - 1 ? 0 : Math.min(max, cont.scrollTop + px)
|
||||
} else {
|
||||
const max = cont.scrollWidth - cont.clientWidth
|
||||
cont.scrollLeft = cont.scrollLeft >= max - 1 ? 0 : Math.min(max, cont.scrollLeft + px)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ axis, steps, px },
|
||||
)
|
||||
}
|
||||
|
||||
async function fpsOf(page, args) {
|
||||
return page.evaluate(
|
||||
async ({ axis, steps, px }) => {
|
||||
const cont = document.querySelector('.stk-table')
|
||||
const frames = []
|
||||
let last = performance.now()
|
||||
let stop = false
|
||||
const tick = () => {
|
||||
const now = performance.now()
|
||||
frames.push(now - last)
|
||||
last = now
|
||||
if (!stop) requestAnimationFrame(tick)
|
||||
}
|
||||
requestAnimationFrame(tick)
|
||||
for (let i = 0; i < steps; i += 1) {
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
if (axis === 'y') {
|
||||
const max = cont.scrollHeight - cont.clientHeight
|
||||
cont.scrollTop = cont.scrollTop >= max - 1 ? 0 : Math.min(max, cont.scrollTop + px)
|
||||
} else {
|
||||
const max = cont.scrollWidth - cont.clientWidth
|
||||
cont.scrollLeft = cont.scrollLeft >= max - 1 ? 0 : Math.min(max, cont.scrollLeft + px)
|
||||
}
|
||||
}
|
||||
stop = true
|
||||
const sorted = [...frames].sort((a, b) => a - b)
|
||||
const avg = frames.reduce((a, b) => a + b, 0) / frames.length
|
||||
const q = (arr, p) => arr[Math.min(arr.length - 1, Math.floor(arr.length * p))]
|
||||
return {
|
||||
fps: +(1000 / avg).toFixed(1),
|
||||
avgMs: +avg.toFixed(2),
|
||||
p95Ms: +q(sorted, 0.95).toFixed(2),
|
||||
maxMs: +sorted[sorted.length - 1].toFixed(1),
|
||||
}
|
||||
},
|
||||
args,
|
||||
)
|
||||
}
|
||||
|
||||
console.log('================= 打开页面 =================')
|
||||
const { browser, context, page } = await makePage()
|
||||
|
||||
/* ---------- Q5: 初始挂载成本(errorCount 全表校验) ---------- */
|
||||
const mount = await page.evaluate((src) => {
|
||||
const pageInst = (() => {
|
||||
const el = document.querySelector('.booking')
|
||||
let i = el?.__vueParentComponent
|
||||
while (i && !i.setupState?.rows) i = i.parent
|
||||
return i
|
||||
})()
|
||||
const out = { found: !!pageInst }
|
||||
if (pageInst) {
|
||||
const ss = pageInst.setupState
|
||||
const rows = ss.rows?.value ?? ss.rows
|
||||
const cols = ss.columns?.value ?? ss.columns
|
||||
out.rows = rows?.length
|
||||
out.cols = cols?.length
|
||||
// 计时:把 errorCount 弄脏后重读,等于跑一遍「1000 行 × 21 列」的校验
|
||||
const t0 = performance.now()
|
||||
const v = ss.errorCount?.value
|
||||
out.errorCountCachedReadMs = +(performance.now() - t0).toFixed(2)
|
||||
out.errorCount = v
|
||||
// 弄脏一行 → 重算
|
||||
if (rows?.length) {
|
||||
rows[0].freight = Number(rows[0].freight) + 0.001
|
||||
const t1 = performance.now()
|
||||
void ss.errorCount?.value
|
||||
out.errorCountRecomputeMs = +(performance.now() - t1).toFixed(2)
|
||||
rows[0].freight = Number(rows[0].freight) - 0.001
|
||||
}
|
||||
}
|
||||
return out
|
||||
}, INST)
|
||||
console.log('Q5 页面状态/errorCount 重算:', JSON.stringify(mount))
|
||||
|
||||
/* ---------- Q1+Q2: 纵向滚动 —— JS vs 样式布局 ---------- */
|
||||
console.log('\n================= Q1 纵向滚动:JS vs 样式/布局 =================')
|
||||
const yMetrics = await metricsDuring(page, context, () => doScroll(page, { axis: 'y', steps: 120 }))
|
||||
console.log('纵向 120 帧指标:', JSON.stringify(yMetrics))
|
||||
console.log(
|
||||
` → 每帧:Script ${(yMetrics.ScriptDuration / 120 * 1000).toFixed(1)}ms | Style ${(yMetrics.StyleDuration / 120 * 1000).toFixed(1)}ms | Layout ${(yMetrics.LayoutDuration / 120 * 1000).toFixed(1)}ms | 重算样式 ${(yMetrics.RecalcStyleCount / 120).toFixed(1)} 次/帧 | 布局 ${(yMetrics.LayoutCount / 120).toFixed(1)} 次/帧`,
|
||||
)
|
||||
|
||||
/* ---------- Q2: 横向滚动期间的 DOM 变更计数 ---------- */
|
||||
console.log('\n================= Q2 横向滚动:本不该发生的 DOM 变更 =================')
|
||||
const mutX = await page.evaluate(async () => {
|
||||
const cont = document.querySelector('.stk-table')
|
||||
const tbody = document.querySelector('.stk-tbody-main')
|
||||
let childList = 0
|
||||
let attrs = 0
|
||||
let charData = 0
|
||||
const changedAttrs = {}
|
||||
const obs = new MutationObserver((records) => {
|
||||
for (const r of records) {
|
||||
if (r.type === 'childList') childList += r.addedNodes.length + r.removedNodes.length
|
||||
else if (r.type === 'attributes') {
|
||||
attrs += 1
|
||||
changedAttrs[r.attributeName] = (changedAttrs[r.attributeName] || 0) + 1
|
||||
} else charData += 1
|
||||
}
|
||||
})
|
||||
obs.observe(tbody, { childList: true, subtree: true, attributes: true, characterData: true })
|
||||
const FRAMES = 60
|
||||
for (let i = 0; i < FRAMES; i += 1) {
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
const max = cont.scrollWidth - cont.clientWidth
|
||||
cont.scrollLeft = cont.scrollLeft >= max - 1 ? 0 : Math.min(max, cont.scrollLeft + 60)
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
obs.disconnect()
|
||||
return { FRAMES, childList, attrs, charData, changedAttrs }
|
||||
})
|
||||
console.log('横向 60 帧 tbody 变更:', JSON.stringify(mutX))
|
||||
console.log(
|
||||
` → 每帧:节点增删 ${(mutX.childList / mutX.FRAMES).toFixed(1)} | 属性写入 ${(mutX.attrs / mutX.FRAMES).toFixed(1)}`,
|
||||
)
|
||||
|
||||
/* ---------- Q2b: 纵向滚动 DOM 变更 ---------- */
|
||||
const mutY = await page.evaluate(async () => {
|
||||
const cont = document.querySelector('.stk-table')
|
||||
const tbody = document.querySelector('.stk-tbody-main')
|
||||
let childList = 0
|
||||
let attrs = 0
|
||||
const obs = new MutationObserver((records) => {
|
||||
for (const r of records) {
|
||||
if (r.type === 'childList') childList += r.addedNodes.length + r.removedNodes.length
|
||||
else if (r.type === 'attributes') attrs += 1
|
||||
}
|
||||
})
|
||||
obs.observe(tbody, { childList: true, subtree: true, attributes: true, characterData: true })
|
||||
const FRAMES = 60
|
||||
for (let i = 0; i < FRAMES; i += 1) {
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
const max = cont.scrollHeight - cont.clientHeight
|
||||
cont.scrollTop = cont.scrollTop >= max - 1 ? 0 : Math.min(max, cont.scrollTop + 60)
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
obs.disconnect()
|
||||
return { FRAMES, childList, attrs }
|
||||
})
|
||||
console.log('纵向 60 帧 tbody 变更:', JSON.stringify(mutY))
|
||||
console.log(
|
||||
` → 每帧:节点增删 ${(mutY.childList / mutY.FRAMES).toFixed(1)} | 属性写入 ${(mutY.attrs / mutY.FRAMES).toFixed(1)}`,
|
||||
)
|
||||
|
||||
/* ---------- Q3: CSS 层归因(注入覆盖做 A/B) ---------- */
|
||||
console.log('\n================= Q3 CSS 归因(!important 覆盖 A/B)=================')
|
||||
console.log('基线 纵向:', JSON.stringify(await fpsOf(page, { axis: 'y', steps: 100, px: 60 })))
|
||||
console.log('基线 横向:', JSON.stringify(await fpsOf(page, { axis: 'x', steps: 80, px: 80 })))
|
||||
|
||||
async function cssAB(label, css, measAxis = 'y') {
|
||||
await page.addStyleTag({ content: css })
|
||||
await page.waitForTimeout(500)
|
||||
const y = await fpsOf(page, { axis: 'y', steps: 100, px: 60 })
|
||||
const x = await fpsOf(page, { axis: 'x', steps: 80, px: 80 })
|
||||
console.log(`${label} | 纵 ${y.fps}fps (${y.avgMs}ms) | 横 ${x.fps}fps (${x.avgMs}ms)`)
|
||||
// 移除刚注入的样式
|
||||
await page.evaluate(() => {
|
||||
const tags = [...document.querySelectorAll('style')]
|
||||
const last = tags[tags.length - 1]
|
||||
if (last && last.dataset.probe) last.remove()
|
||||
})
|
||||
await page.waitForTimeout(300)
|
||||
return { y, x }
|
||||
}
|
||||
|
||||
// 用一个带标记的注入函数,保证能精确移除
|
||||
async function cssAB2(label, css) {
|
||||
await page.evaluate((c) => {
|
||||
const s = document.createElement('style')
|
||||
s.dataset.probe = '1'
|
||||
s.textContent = c
|
||||
document.head.appendChild(s)
|
||||
}, css)
|
||||
await page.waitForTimeout(500)
|
||||
const y = await fpsOf(page, { axis: 'y', steps: 100, px: 60 })
|
||||
const x = await fpsOf(page, { axis: 'x', steps: 80, px: 80 })
|
||||
console.log(`${label} | 纵 ${y.fps}fps (${y.avgMs}ms) | 横 ${x.fps}fps (${x.avgMs}ms)`)
|
||||
await page.evaluate(() => document.querySelector('style[data-probe]')?.remove())
|
||||
await page.waitForTimeout(300)
|
||||
return { y, x }
|
||||
}
|
||||
|
||||
await cssAB2('① 去掉单元格背景渐变(格线)', `
|
||||
.stk-table td, .stk-table th { background-image: none !important; }
|
||||
`)
|
||||
await cssAB2('② 去掉全部 box-shadow(含 100vmax 叠色)', `
|
||||
.stk-table td, .stk-table th, .stk-table tr { box-shadow: none !important; }
|
||||
`)
|
||||
await cssAB2('③ 去掉 sticky 固定列定位', `
|
||||
.stk-table td.fixed-cell, .stk-table th.fixed-cell { position: static !important; }
|
||||
`)
|
||||
await cssAB2('④ 去掉单元格 padding/文字溢出省略', `
|
||||
.stk-table td, .stk-table th { padding: 0 !important; }
|
||||
.stk-table .table-cell-wrapper { overflow: visible !important; text-overflow: clip !important; white-space: normal !important; }
|
||||
`)
|
||||
await cssAB2('⑤ ①②③④ 全上', `
|
||||
.stk-table td, .stk-table th { background-image: none !important; box-shadow: none !important; }
|
||||
.stk-table td.fixed-cell, .stk-table th.fixed-cell { position: static !important; }
|
||||
.stk-table td, .stk-table th { padding: 0 !important; }
|
||||
.stk-table .table-cell-wrapper { overflow: visible !important; text-overflow: clip !important; white-space: normal !important; }
|
||||
`)
|
||||
|
||||
/* ---------- Q4: 点击一次编辑的分阶段耗时 ---------- */
|
||||
console.log('\n================= Q4 点击进入编辑:分阶段 =================')
|
||||
const clickPhases = await page.evaluate(async () => {
|
||||
const cont = document.querySelector('.stk-table')
|
||||
cont.scrollTop = 0
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
|
||||
const out = []
|
||||
const trs = [...document.querySelectorAll('.stk-tbody-main tr')].slice(1, 9)
|
||||
for (const tr of trs) {
|
||||
const td = [...tr.querySelectorAll('td')][6]
|
||||
if (!td) continue
|
||||
const r = td.getBoundingClientRect()
|
||||
if (r.top < 40 || r.bottom > window.innerHeight) continue
|
||||
const cx = r.left + r.width / 2
|
||||
const cy = r.top + r.height / 2
|
||||
const t0 = performance.now()
|
||||
|
||||
// 阶段 1:派发点击 → 事件同步处理完
|
||||
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
||||
const Ctor = type.startsWith('pointer') ? PointerEvent : MouseEvent
|
||||
td.dispatchEvent(
|
||||
new Ctor(type, { bubbles: true, cancelable: true, clientX: cx, clientY: cy, button: 0 }),
|
||||
)
|
||||
}
|
||||
const tDispatch = performance.now() - t0
|
||||
|
||||
// 阶段 2:等 Vue 把编辑器插入 DOM(rAF 轮询)
|
||||
let tRender = -1
|
||||
const deadline = performance.now() + 3000
|
||||
while (performance.now() < deadline) {
|
||||
if (td.querySelector('.g3-table__cell-editor-root')) {
|
||||
tRender = performance.now() - t0
|
||||
break
|
||||
}
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
}
|
||||
// 阶段 3:再等一帧,让布局/绘制稳定
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
const tStable = performance.now() - t0
|
||||
|
||||
out.push({
|
||||
dispatchMs: +tDispatch.toFixed(1),
|
||||
renderDoneMs: +tRender.toFixed(1),
|
||||
stableMs: +tStable.toFixed(1),
|
||||
})
|
||||
await new Promise((r) => setTimeout(r, 150))
|
||||
}
|
||||
return out
|
||||
})
|
||||
console.log('点击分阶段(ms):')
|
||||
for (const p of clickPhases) console.log(' ', JSON.stringify(p))
|
||||
const med = (arr) => {
|
||||
const s = [...arr].sort((a, b) => a - b)
|
||||
return s.length ? +s[Math.floor(s.length / 2)].toFixed(1) : null
|
||||
}
|
||||
console.log(
|
||||
` 中位:事件处理 ${med(clickPhases.map((p) => p.dispatchMs))}ms | 编辑器进 DOM ${med(clickPhases.map((p) => p.renderDoneMs))}ms | 稳定 ${med(clickPhases.map((p) => p.stableMs))}ms`,
|
||||
)
|
||||
|
||||
/* ---------- Q4b: 点同一格 vs 换格 ---------- */
|
||||
console.log('\n================= Q4b 同格重复点击(隔离「首次挂载」成本)=================')
|
||||
const repeat = await page.evaluate(async () => {
|
||||
const cont = document.querySelector('.stk-table')
|
||||
cont.scrollTop = 0
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
const td = [...document.querySelectorAll('.stk-tbody-main tr')][3]?.querySelectorAll('td')[6]
|
||||
if (!td) return { error: 'no td' }
|
||||
const r = td.getBoundingClientRect()
|
||||
const cx = r.left + r.width / 2
|
||||
const cy = r.top + r.height / 2
|
||||
const fire = () => {
|
||||
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
||||
const Ctor = type.startsWith('pointer') ? PointerEvent : MouseEvent
|
||||
td.dispatchEvent(
|
||||
new Ctor(type, { bubbles: true, cancelable: true, clientX: cx, clientY: cy, button: 0 }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const timings = []
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
const t0 = performance.now()
|
||||
fire()
|
||||
let done = -1
|
||||
const deadline = performance.now() + 3000
|
||||
while (performance.now() < deadline) {
|
||||
if (td.querySelector('.g3-table__cell-editor-root')) {
|
||||
done = performance.now() - t0
|
||||
break
|
||||
}
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
}
|
||||
timings.push(+done.toFixed(1))
|
||||
await new Promise((r) => setTimeout(r, 200))
|
||||
}
|
||||
return { timings, editorRootsNow: document.querySelectorAll('.g3-table__cell-editor-root').length }
|
||||
})
|
||||
console.log('同格连点 6 次(ms):', JSON.stringify(repeat))
|
||||
|
||||
/* ---------- Q4c: 编辑态下的每键成本 ---------- */
|
||||
console.log('\n================= Q4c 编辑态:敲一个字符的延迟 =================')
|
||||
const keyCost = await page.evaluate(async () => {
|
||||
const input = document.querySelector('.g3-table__cell-editor-root input')
|
||||
if (!input) return { error: 'no input (该列可能不是 text)' }
|
||||
const timings = []
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
const t0 = performance.now()
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }))
|
||||
input.value = `${input.value}a`
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
timings.push(+(performance.now() - t0).toFixed(1))
|
||||
await new Promise((r) => setTimeout(r, 120))
|
||||
}
|
||||
return { timings }
|
||||
})
|
||||
console.log('每键 → 下一帧(ms):', JSON.stringify(keyCost))
|
||||
|
||||
/* ---------- 编辑态滚动再测一次 + 指标 ---------- */
|
||||
console.log('\n================= 编辑态:纵向滚动 + 指标 =================')
|
||||
const em = await metricsDuring(page, context, () => doScroll(page, { axis: 'y', steps: 100 }))
|
||||
const efps = await fpsOf(page, { axis: 'y', steps: 100, px: 60 })
|
||||
console.log('编辑态 纵向:', JSON.stringify(efps))
|
||||
console.log('编辑态 100 帧指标:', JSON.stringify(em))
|
||||
const edDom = await page.evaluate(() => ({
|
||||
editorRoots: document.querySelectorAll('.g3-table__cell-editor-root').length,
|
||||
inputs: document.querySelectorAll('.g3-table__cell-editor-root.g3-input').length,
|
||||
selects: document.querySelectorAll('.g3-table__cell-editor-root.g3-select').length,
|
||||
tds: document.querySelectorAll('.stk-tbody-main td').length,
|
||||
}))
|
||||
console.log('编辑态 DOM:', JSON.stringify(edDom))
|
||||
|
||||
await browser.close()
|
||||
Reference in new issue
Block a user