203 lines
11 KiB
JavaScript
203 lines
11 KiB
JavaScript
const assert = require('assert')
|
|
|
|
const url = 'http://localhost:5173/#/pages/auth/a01-entry'
|
|
const sizes = [
|
|
{ width: 320, height: 568 },
|
|
{ width: 360, height: 616 },
|
|
{ width: 360, height: 640 },
|
|
{ width: 360, height: 800 },
|
|
{ width: 412, height: 915 },
|
|
{ width: 480, height: 1040 }
|
|
]
|
|
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
|
|
|
|
const connect = async () => {
|
|
const port = process.env.CHROME_DEBUGGING_PORT || '9222'
|
|
const pages = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json()
|
|
const page = pages.find((item) => item.type === 'page' && item.url.startsWith('http://localhost:5173'))
|
|
if (!page) throw new Error('Chrome debugging has no application page')
|
|
|
|
const socket = new WebSocket(page.webSocketDebuggerUrl)
|
|
await new Promise((resolve, reject) => {
|
|
socket.addEventListener('open', resolve, { once: true })
|
|
socket.addEventListener('error', reject, { once: true })
|
|
})
|
|
|
|
let id = 0
|
|
const pending = new Map()
|
|
const exceptions = []
|
|
socket.addEventListener('message', (event) => {
|
|
const message = JSON.parse(event.data)
|
|
if (message.method === 'Runtime.exceptionThrown') exceptions.push(message.params.exceptionDetails.text)
|
|
const request = pending.get(message.id)
|
|
if (!request) return
|
|
pending.delete(message.id)
|
|
if (message.error) request.reject(new Error(message.error.message))
|
|
else request.resolve(message.result)
|
|
})
|
|
|
|
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
|
id += 1
|
|
pending.set(id, { resolve, reject })
|
|
socket.send(JSON.stringify({ id, method, params }))
|
|
})
|
|
|
|
return { socket, send, exceptions }
|
|
}
|
|
|
|
const valueOf = async (send, expression) => {
|
|
const result = await send('Runtime.evaluate', { expression, returnByValue: true })
|
|
return result.result?.value
|
|
}
|
|
|
|
const waitFor = async (send, expression, message) => {
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
if (await valueOf(send, expression)) return
|
|
await sleep(100)
|
|
}
|
|
throw new Error(message)
|
|
}
|
|
|
|
const waitForFreshDocument = async (send, previousTimeOrigin) => {
|
|
await waitFor(
|
|
send,
|
|
`performance.timeOrigin !== ${JSON.stringify(previousTimeOrigin)}`,
|
|
'A01 navigation did not create a fresh document'
|
|
)
|
|
}
|
|
|
|
const run = async () => {
|
|
const { socket, send, exceptions } = await connect()
|
|
try {
|
|
await send('Page.enable')
|
|
await send('Runtime.enable')
|
|
|
|
for (const size of sizes) {
|
|
await send('Emulation.setDeviceMetricsOverride', {
|
|
width: size.width,
|
|
height: size.height,
|
|
deviceScaleFactor: 1,
|
|
mobile: true
|
|
})
|
|
await send('Page.navigate', { url })
|
|
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `A01 did not navigate at ${size.width}x${size.height}`)
|
|
const previousTimeOrigin = await valueOf(send, 'performance.timeOrigin')
|
|
await send('Page.reload')
|
|
await waitForFreshDocument(send, previousTimeOrigin)
|
|
await waitFor(send, "document.querySelectorAll('.login-tab').length === 2", `A01 did not render at ${size.width}x${size.height}`)
|
|
await waitFor(send, "document.querySelector('.auth-shell__header-image img')?.naturalWidth === 824", `A01 header did not load at ${size.width}x${size.height}`)
|
|
const metrics = await valueOf(send, `(() => {
|
|
const root = document.querySelector('.auth-page')
|
|
const header = document.querySelector('.auth-shell__header')
|
|
const headerImage = document.querySelector('.auth-shell__header-image img')
|
|
const paper = document.querySelector('.auth-shell__paper')
|
|
const content = document.querySelector('.login-content')
|
|
const headerRect = header?.getBoundingClientRect()
|
|
const paperRect = paper?.getBoundingClientRect()
|
|
const contentRect = content?.getBoundingClientRect()
|
|
return {
|
|
innerWidth: window.innerWidth,
|
|
innerHeight: window.innerHeight,
|
|
scrollWidth: document.documentElement.scrollWidth,
|
|
documentScrollHeight: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
|
|
rootScrollHeight: root?.scrollHeight,
|
|
headerBottom: headerRect?.bottom,
|
|
headerImageWidth: headerImage?.getBoundingClientRect().width,
|
|
headerImageHeight: headerImage?.getBoundingClientRect().height,
|
|
headerNaturalWidth: headerImage?.naturalWidth,
|
|
headerNaturalHeight: headerImage?.naturalHeight,
|
|
paperTop: paperRect?.top,
|
|
contentTop: contentRect?.top,
|
|
agreementBottom: document.querySelector('.agreement-area')?.getBoundingClientRect().bottom
|
|
}
|
|
})()`)
|
|
assert.strictEqual(metrics.innerWidth, size.width, `A01 viewport width mismatch at ${size.width}x${size.height}`)
|
|
assert(metrics.scrollWidth <= size.width + 1, `A01 has horizontal overflow at ${size.width}x${size.height}: ${metrics.scrollWidth}`)
|
|
assert.deepStrictEqual([metrics.headerNaturalWidth, metrics.headerNaturalHeight], [824, 340], `A01 header asset mismatch at ${size.width}x${size.height}`)
|
|
assert(Math.abs(metrics.headerImageHeight / metrics.headerImageWidth - 340 / 824) < 0.01, `A01 header is distorted at ${size.width}x${size.height}`)
|
|
assert(metrics.paperTop >= metrics.headerBottom - 1, `A01 paper overlaps the header at ${size.width}x${size.height}`)
|
|
assert(metrics.contentTop >= metrics.paperTop, `A01 content escapes paper flow at ${size.width}x${size.height}`)
|
|
if (size.width >= 360 && size.height >= 640) {
|
|
assert(metrics.rootScrollHeight <= metrics.innerHeight + 1, `A01 SMS state must fit at ${size.width}x${size.height}`)
|
|
assert(metrics.agreementBottom <= metrics.innerHeight + 1, `A01 SMS agreement must remain visible at ${size.width}x${size.height}`)
|
|
} else {
|
|
assert(metrics.agreementBottom <= metrics.documentScrollHeight + 1, `A01 SMS agreement must remain reachable at ${size.width}x${size.height}`)
|
|
}
|
|
|
|
const buttonAssets = await valueOf(send, `Array.from(document.querySelectorAll('.button-skin img')).map((image) => ({
|
|
src: image.currentSrc || image.src,
|
|
naturalWidth: image.naturalWidth,
|
|
naturalHeight: image.naturalHeight
|
|
}))`)
|
|
assert.strictEqual(buttonAssets.length, 2, `A01 button skins did not render at ${size.width}x${size.height}`)
|
|
assert(buttonAssets[0].src.includes('a01-scroll-primary-v3.png'), `A01 primary v3 skin is missing at ${size.width}x${size.height}`)
|
|
assert(buttonAssets[1].src.includes('a01-scroll-secondary-v3.png'), `A01 secondary v3 skin is missing at ${size.width}x${size.height}`)
|
|
assert.deepStrictEqual(
|
|
buttonAssets.map(({ naturalWidth, naturalHeight }) => [naturalWidth, naturalHeight]),
|
|
[[1866, 276], [1866, 300]],
|
|
`A01 v3 button assets have unexpected runtime dimensions at ${size.width}x${size.height}`
|
|
)
|
|
await valueOf(send, "document.querySelectorAll('.login-tab')[1].click()")
|
|
await waitFor(send, "document.querySelectorAll('.login-tab')[1].classList.contains('active')", `A01 SMS tab did not activate at ${size.width}x${size.height}`)
|
|
const smsMetrics = await valueOf(send, `({
|
|
documentScrollHeight: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight),
|
|
rootScrollHeight: document.querySelector('.auth-page')?.scrollHeight,
|
|
agreementBottom: document.querySelector('.agreement-area')?.getBoundingClientRect().bottom
|
|
})`)
|
|
if (size.width >= 360 && size.height >= 640) {
|
|
assert(smsMetrics.rootScrollHeight <= metrics.innerHeight + 1, `A01 SMS state must fit at ${size.width}x${size.height}`)
|
|
assert(smsMetrics.agreementBottom <= metrics.innerHeight + 1, `A01 SMS agreement must remain visible at ${size.width}x${size.height}`)
|
|
} else {
|
|
assert(smsMetrics.agreementBottom <= smsMetrics.documentScrollHeight + 1, `A01 SMS agreement must remain reachable at ${size.width}x${size.height}`)
|
|
}
|
|
await valueOf(send, "document.querySelectorAll('.login-tab')[0].click()")
|
|
await waitFor(send, "document.querySelectorAll('.login-tab')[0].classList.contains('active')", `A01 password tab did not activate at ${size.width}x${size.height}`)
|
|
assert.strictEqual(await valueOf(send, "Boolean(document.querySelector('.password-toggle'))"), true, `A01 password form did not render at ${size.width}x${size.height}`)
|
|
}
|
|
|
|
await valueOf(send, "document.querySelector('.login-submit').click()")
|
|
await waitFor(send, "Boolean(document.querySelector('.app-toast'))", 'A01 invalid phone Toast did not render')
|
|
const toastBorderImage = await valueOf(send, "getComputedStyle(document.querySelector('.app-toast')).borderImageSource")
|
|
assert(toastBorderImage.includes('a01-scroll-toast-v3.png'), `A01 Toast did not render the v3 nine-slice asset: ${toastBorderImage}`)
|
|
|
|
await valueOf(send, "document.querySelectorAll('.login-tab')[1].click()")
|
|
await waitFor(send, "document.querySelectorAll('.login-tab')[1].classList.contains('active')", 'A01 SMS tab did not activate')
|
|
await waitFor(send, "Boolean(document.querySelector('.input-icon--sms img'))", 'A01 SMS field image did not render after tab activation')
|
|
const smsIconSource = await valueOf(send, "document.querySelector('.input-icon--sms img')?.getAttribute('src')")
|
|
assert(smsIconSource?.includes('a01-icon-sms-code-v2.png'), `A01 SMS state did not use the approved message icon: ${smsIconSource}`)
|
|
assert.strictEqual(await valueOf(send, "Boolean(document.querySelector('.password-toggle'))"), false, 'A01 SMS state retained the password eye')
|
|
assert(await valueOf(send, "Boolean(document.querySelector('.forgot-password'))"), 'A01 SMS state must retain the password-recovery entry')
|
|
|
|
await valueOf(send, `(() => {
|
|
const inputs = document.querySelectorAll('.auth-input input')
|
|
inputs[0].value = '13800138000'
|
|
inputs[0].dispatchEvent(new Event('input', { bubbles: true }))
|
|
inputs[1].value = '1234'
|
|
inputs[1].dispatchEvent(new Event('input', { bubbles: true }))
|
|
document.querySelector('.login-submit').click()
|
|
})()`)
|
|
await waitFor(send, "Boolean(document.querySelector('.agreement-error'))", 'A01 did not show inline agreement validation')
|
|
await valueOf(send, "document.querySelector('.agreement-toggle').click()")
|
|
await waitFor(send, "!document.querySelector('.agreement-error')", 'A01 agreement error did not clear after selection')
|
|
await valueOf(send, "document.querySelector('.login-submit').click()")
|
|
await waitFor(
|
|
send,
|
|
"document.querySelector('.app-toast__copy')?.textContent === '当前为本地预览模式,真实认证服务未启用'",
|
|
'A01 local preview did not reject a fake SMS login'
|
|
)
|
|
assert.strictEqual(await valueOf(send, "Boolean(document.querySelector('.verification-layer'))"), false, 'A01 retained the obsolete fake verification layer')
|
|
assert.strictEqual(await valueOf(send, "Boolean(document.querySelector('.tac-layer--visible'))"), false, 'A01 opened TAC without a server-bindable password-login ticket')
|
|
assert.deepStrictEqual(exceptions, [], `A01 raised browser exceptions: ${exceptions.join('; ')}`)
|
|
|
|
process.stdout.write('A01-RESPONSIVE-RUNTIME-SMOKE PASS\n')
|
|
} finally {
|
|
try { await send('Emulation.clearDeviceMetricsOverride') } catch (_) {}
|
|
socket.close()
|
|
}
|
|
}
|
|
|
|
run().catch((error) => {
|
|
process.stderr.write(`${error.stack || error.message}\n`)
|
|
process.exit(1)
|
|
})
|