Review changes batch 5 of 6

This commit is contained in:
2026-07-20 06:52:26 +08:00
parent 0d0645a112
commit db97d3da27
41 changed files with 6672 additions and 1124 deletions
+278 -8
View File
@@ -6,7 +6,7 @@ const renderSettleMilliseconds = 900
const chromeDebuggingPort = process.env.CHROME_DEBUGGING_PORT || '9222'
if (!url || !selector || !output) {
throw new Error('Usage: node scripts/capture-chrome-page.js <url> <selector> <output> [width] [height] [password-tab|sms-tab|invalid-login|verification-dialog|g06-results|g06-empty|g06-invite|fresh-navigation]')
throw new Error('Usage: node scripts/capture-chrome-page.js <url> <selector> <output> [width] [height] [password-tab|sms-tab|invalid-login|verification-dialog|g06-results|g06-empty|g06-invite|g03-*|fresh-navigation]')
}
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
@@ -149,9 +149,9 @@ const waitForFreshDocument = async (send, documentTimeOrigin) => {
}
const prepareG06State = async (send, action) => {
if (action !== 'g06-results' && action !== 'g06-empty' && action !== 'g06-invite') return
if (action !== 'g06-results' && action !== 'g06-empty' && action !== 'g06-error' && action !== 'g06-invite' && action !== 'g06-invite-invalid') return
if (action === 'g06-invite') {
if (action === 'g06-invite' || action === 'g06-invite-invalid') {
const switched = await send('Runtime.evaluate', {
expression: `(() => {
const tab = document.querySelectorAll('.mode-tab')[1]
@@ -168,7 +168,7 @@ const prepareG06State = async (send, action) => {
const input = document.querySelector('.invite-input input')
const button = document.querySelector('.invite-controls .search-action')
if (!input || !button) return false
input.value = 'JP2026'
input.value = ${JSON.stringify(action === 'g06-invite' ? 'JP2026' : 'INVALID')}
input.dispatchEvent(new Event('input', { bubbles: true }))
return true
})()`,
@@ -179,7 +179,7 @@ const prepareG06State = async (send, action) => {
await send('Runtime.evaluate', { expression: "document.querySelector('.invite-controls .search-action').click()" })
} else {
const keyword = action === 'g06-results' ? '汤' : '不存在的家谱'
const keyword = action === 'g06-results' ? '汤' : action === 'g06-error' ? '失败' : '不存在的家谱'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const input = document.querySelector('.search-input input')
@@ -195,7 +195,7 @@ const prepareG06State = async (send, action) => {
await send('Runtime.evaluate', { expression: "document.querySelector('.search-action').click()" })
}
const stateSelector = action === 'g06-results' ? '.search-results .genealogy-card' : action === 'g06-empty' ? '.search-empty' : '.invite-result .genealogy-card'
const stateSelector = action === 'g06-results' ? '.search-results .genealogy-card' : action === 'g06-empty' ? '.search-empty' : action === 'g06-error' ? '.search-error' : action === 'g06-invite-invalid' ? '.invite-invalid' : '.invite-result .genealogy-card'
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `Boolean(document.querySelector(${JSON.stringify(stateSelector)}))`,
@@ -207,6 +207,267 @@ const prepareG06State = async (send, action) => {
}
}
const waitForPageLoad = async (socket, trigger) => new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
socket.removeEventListener('message', onMessage)
reject(new Error('Page reload did not emit Page.loadEventFired'))
}, 4000)
const onMessage = (event) => {
const message = JSON.parse(event.data)
if (message.method !== 'Page.loadEventFired') return
clearTimeout(timeout)
socket.removeEventListener('message', onMessage)
resolve()
}
socket.addEventListener('message', onMessage)
Promise.resolve().then(trigger).catch((error) => {
clearTimeout(timeout)
socket.removeEventListener('message', onMessage)
reject(error)
})
})
const waitForSelector = async (send, selector, message) => {
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `Boolean(document.querySelector(${JSON.stringify(selector)}))`,
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error(message)
await sleep(100)
}
}
const waitForText = async (send, selector, text, message) => {
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: `document.querySelector(${JSON.stringify(selector)})?.textContent.includes(${JSON.stringify(text)}) === true`,
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error(message)
await sleep(50)
}
}
const prepareG03State = async (send, action) => {
const actions = new Set([
'g03-create-validation',
'g03-duplicate-reminder',
'g03-create-submitting',
'g03-create-error',
'g03-ancestor-validation',
'g03-ancestor-submitting',
'g03-ancestor-error',
'g03-ancestor-success'
])
if (!actions.has(action)) return
if (action === 'g03-create-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.flow-primary-action').click()" })
await waitForSelector(send, '.field-error', 'G03 create validation did not render')
return
}
if (action.startsWith('g03-create') || action === 'g03-duplicate-reminder') {
const name = action === 'g03-create-error' ? '失败' : '汤氏家谱'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const inputs = document.querySelectorAll('.field-row input')
if (inputs.length < 4) return false
const values = ['汤', ${JSON.stringify(name)}, '', '河南·洛阳']
inputs.forEach((input, index) => {
input.value = values[index]
input.dispatchEvent(new Event('input', { bubbles: true }))
})
document.querySelector('.flow-primary-action').click()
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error(`Could not prepare ${action}`)
await waitForSelector(send, '.duplicate-reminder', `G03 duplicate reminder did not render for ${action}`)
if (action === 'g03-duplicate-reminder') return
await send('Runtime.evaluate', { expression: "document.querySelector('.duplicate-reminder__confirm').click()" })
if (action === 'g03-create-submitting') {
await waitForText(send, '.flow-primary-action__copy', '正在创建', 'G03 create submitting state did not render')
return
}
await waitForSelector(send, '.flow-error', 'G03 create error state did not render')
return
}
if (action === 'g03-ancestor-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.flow-primary-action').click()" })
await waitForSelector(send, '.field-error', 'G03 ancestor validation did not render')
return
}
const personName = action === 'g03-ancestor-error' ? '失败' : '汤远'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const input = document.querySelector('.field-row input')
if (!input) return false
input.value = ${JSON.stringify(personName)}
input.dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('.flow-primary-action').click()
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error(`Could not prepare ${action}`)
if (action === 'g03-ancestor-submitting') {
await waitForText(send, '.flow-primary-action__copy', '正在保存', 'G03 ancestor submitting state did not render')
} else if (action === 'g03-ancestor-error') {
await waitForSelector(send, '.flow-error', 'G03 ancestor error state did not render')
} else {
await waitForSelector(send, '.flow-success-layer', 'G03 ancestor success state did not render')
}
}
const prepareG08State = async (send, action) => {
if (!['g08-validation', 'g08-submitting', 'g08-error', 'g08-success'].includes(action)) return
if (action === 'g08-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.join-action').click()" })
await waitForSelector(send, '.join-field-error', 'G08 validation errors did not render')
return
}
const realName = action === 'g08-error' ? '失败' : '汤明'
const prepared = await send('Runtime.evaluate', {
expression: `(() => {
const inputs = document.querySelectorAll('.join-field input')
if (inputs.length < 2) return false
const values = [${JSON.stringify(realName)}, '堂侄']
inputs.forEach((input, index) => {
input.value = values[index]
input.dispatchEvent(new Event('input', { bubbles: true }))
})
return true
})()`,
returnByValue: true
})
if (!prepared.result?.value) throw new Error(`Could not prepare ${action}`)
await sleep(50)
await send('Runtime.evaluate', { expression: "document.querySelector('.join-action').click()" })
if (action === 'g08-submitting') {
await waitForText(send, '.join-action', '正在', 'G08 submitting state did not render')
} else if (action === 'g08-error') {
await waitForSelector(send, '.join-state--error', 'G08 error state did not render')
} else {
await waitForSelector(send, '.join-state--success', 'G08 success state did not render')
}
}
const prepareG09State = async (send, action) => {
if (!['g09-withdraw-dialog', 'g09-withdrawn'].includes(action)) return
await send('Runtime.evaluate', { expression: "document.querySelector('.application-card__action')?.click()" })
await waitForSelector(send, '.app-dialog-layer', 'G09 withdrawal dialog did not render')
if (action === 'g09-withdrawn') {
await send('Runtime.evaluate', { expression: "document.querySelector('.app-dialog__actions .app-button:last-child')?.click()" })
await waitForText(send, '.application-card__status', '已撤回', 'G09 withdrawn state did not render')
}
}
const prepareG10State = async (send, action) => {
const actions = ['g10-help', 'g10-approve-dialog', 'g10-approved', 'g10-reject-dialog', 'g10-rejected']
if (!actions.includes(action)) return
if (action === 'g10-help') {
await send('Runtime.evaluate', { expression: "document.querySelector('.header-action')?.click()" })
await waitForText(send, '.app-dialog__title', '审核说明', 'G10 help dialog did not render')
return
}
const isApprove = action === 'g10-approve-dialog' || action === 'g10-approved'
const selector = isApprove ? '.review-action:last-child' : '.review-action:first-child'
await send('Runtime.evaluate', { expression: `document.querySelector(${JSON.stringify(selector)})?.click()` })
await waitForSelector(send, '.app-dialog-layer', 'G10 audit confirmation did not render')
if (action === 'g10-approved' || action === 'g10-rejected') {
await send('Runtime.evaluate', { expression: "document.querySelector('.app-dialog__actions .app-button:last-child')?.click()" })
await waitForText(send, '.application-card__status', isApprove ? '已通过' : '已拒绝', 'G10 audit result did not render')
}
}
const prepareG11State = async (send, action) => {
if (!['g11-validation', 'g11-saved'].includes(action)) return
if (action === 'g11-validation') {
await send('Runtime.evaluate', {
expression: `(() => {
const input = document.querySelector('.settings-field input')
if (!input) return false
input.value = ''
input.dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('.settings-action')?.click()
return true
})()`,
returnByValue: true
})
await waitForSelector(send, '.settings-field-error', 'G11 validation state did not render')
return
}
await send('Runtime.evaluate', { expression: "document.querySelector('.settings-action')?.click()" })
await waitForSelector(send, '.settings-state--success', 'G11 saved state did not render')
await waitForSelector(send, '.settings-feedback', 'G11 saved feedback did not render')
}
const prepareG12State = async (send, action) => {
if (!['g12-validation', 'g12-save-error', 'g12-saved'].includes(action)) return
const value = action === 'g12-validation' ? '' : action === 'g12-save-error' ? '失败' : '启宗敦本继世传芳'
await send('Runtime.evaluate', {
expression: `(() => {
const textarea = document.querySelector('.poem-field textarea')
if (!textarea) return false
textarea.value = ${JSON.stringify(value)}
textarea.dispatchEvent(new Event('input', { bubbles: true }))
document.querySelector('.poem-editor__actions .poem-action:last-child')?.click()
return true
})()`,
returnByValue: true
})
if (action === 'g12-saved') {
await waitForSelector(send, '.poem-state--list', 'G12 saved list did not render')
await waitForSelector(send, '.poem-feedback', 'G12 saved feedback did not render')
} else {
await waitForSelector(send, '.poem-field-error', 'G12 validation or save error did not render')
}
}
const prepareT03T06State = async (send, action) => {
if (action !== 't06-conflict-help') return
await send('Runtime.evaluate', { expression: "document.querySelector('.conflict-actions .member-form-action:last-child')?.click()" })
await waitForSelector(send, '.app-dialog-layer', 'T06 conflict help dialog did not render')
}
const prepareFSeriesState = async (send, action) => {
if (action === 'f02-validation') {
await send('Runtime.evaluate', { expression: "document.querySelector('.publish-form .app-button')?.click()" })
await waitForSelector(send, '.app-toast', 'F02 empty-content toast did not render')
return
}
if (action !== 'module-preview-toast') return
await send('Runtime.evaluate', { expression: "(document.querySelector('.list-card') || document.querySelector('.settings-row'))?.click()" })
await waitForSelector(send, '.app-toast', 'ModulePage preview toast did not render')
}
const prepareNSeriesState = async (send, action) => {
if (action === 'n01-read-first') {
await send('Runtime.evaluate', { expression: "document.querySelector('.notice-card')?.click()" })
for (let attempt = 0; attempt < 30; attempt += 1) {
const result = await send('Runtime.evaluate', {
expression: "document.querySelector('.notice-card')?.textContent.includes('未读提醒') === false",
returnByValue: true
})
if (result.result?.value) return
if (attempt === 29) throw new Error('N01 first notice did not enter the read state')
await sleep(50)
}
return
}
if (action !== 'n01-mark-all') return
await send('Runtime.evaluate', { expression: "document.querySelector('.header-action')?.click()" })
await waitForSelector(send, '.app-toast', 'N01 mark-all-read toast did not render')
}
const capture = async () => {
const { socket, send } = await connect()
const viewportWidth = Number(width)
@@ -231,7 +492,7 @@ const capture = async () => {
await send('Page.navigate', { url })
await waitForRequestedUrl(send, url)
if (action !== 'fresh-navigation') {
await send('Page.reload')
await waitForPageLoad(socket, () => send('Page.reload'))
await waitForFreshDocument(send, documentTimeOrigin)
}
@@ -247,9 +508,18 @@ const capture = async () => {
await prepareA01LoginState(send, action)
await prepareG06State(send, action)
await prepareG03State(send, action)
await prepareG08State(send, action)
await prepareG09State(send, action)
await prepareG10State(send, action)
await prepareG11State(send, action)
await prepareG12State(send, action)
await prepareT03T06State(send, action)
await prepareFSeriesState(send, action)
await prepareNSeriesState(send, action)
await waitForLoadedImages(send)
await sleep(renderSettleMilliseconds)
await sleep(action?.endsWith('-submitting') ? 80 : renderSettleMilliseconds)
const screenshot = await send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false })
fs.mkdirSync(path.dirname(output), { recursive: true })
fs.writeFileSync(output, Buffer.from(screenshot.data, 'base64'))