- Introduced `_recent_days_series_hint` to handle queries related to "最近N天" for time series data aggregation. - Added `_cn_num_to_int` function to convert Chinese numerals to integers for better query parsing. - Updated `_nl_sql_hints` to incorporate the new hint generation logic, ensuring accurate SQL output for recent days queries. - Enhanced documentation to reflect these changes and improve clarity on the new functionalities.
103 lines
4.3 KiB
JavaScript
103 lines
4.3 KiB
JavaScript
import { launch, login, BASE, SHOT_DIR, ACCOUNTS, makeEmitter } from './harness.mjs'
|
|
|
|
const b = await launch()
|
|
const out = []
|
|
const emit = makeEmitter(out) // F-13:边跑边出,中途异常不丢已完成证据
|
|
const ok = (n, pass, d = '') => emit(`${pass ? 'PASS' : 'FAIL'} | ${n}${d ? ' | ' + d : ''}`)
|
|
|
|
const VIEWPORTS = [
|
|
{ w: 1280, h: 900 },
|
|
{ w: 1024, h: 800 },
|
|
{ w: 768, h: 900 },
|
|
{ w: 320, h: 720 },
|
|
]
|
|
|
|
// ---------------- document-level a11y ----------------
|
|
{
|
|
const ctx = await b.newContext({ viewport: { width: 1280, height: 900 } })
|
|
const p = await ctx.newPage()
|
|
await p.goto(`${BASE}/#/login`, { waitUntil: 'domcontentloaded' })
|
|
await p.waitForTimeout(2500)
|
|
const meta = await p.evaluate(() => ({
|
|
title: document.title,
|
|
lang: document.documentElement.lang,
|
|
imgsNoAlt: [...document.images].filter((i) => !i.hasAttribute('alt')).map((i) => i.getAttribute('src')),
|
|
h1: [...document.querySelectorAll('h1')].map((h) => h.innerText.trim()),
|
|
buttonsNoName: [...document.querySelectorAll('button')].filter((b) => !b.innerText.trim() && !b.getAttribute('aria-label')).length,
|
|
}))
|
|
ok('浏览器标签标题有意义', meta.title !== 'web' && meta.title.trim().length > 0, `title=${JSON.stringify(meta.title)}`)
|
|
ok('html lang 与中文界面一致', /^zh/.test(meta.lang), `lang=${JSON.stringify(meta.lang)}`)
|
|
ok('图片都有 alt', meta.imgsNoAlt.length === 0, JSON.stringify(meta.imgsNoAlt))
|
|
ok('登录页有唯一 h1', meta.h1.length === 1, JSON.stringify(meta.h1))
|
|
ok('无无名称按钮', meta.buttonsNoName === 0, `${meta.buttonsNoName} unnamed buttons`)
|
|
await ctx.close()
|
|
}
|
|
|
|
// ---------------- viewport matrix ----------------
|
|
for (const [role, acc] of Object.entries(ACCOUNTS)) {
|
|
const ctx = await b.newContext({ viewport: { width: 1280, height: 900 } })
|
|
const p = await ctx.newPage()
|
|
await login(p, role)
|
|
for (const vp of VIEWPORTS) {
|
|
await p.setViewportSize({ width: vp.w, height: vp.h })
|
|
await p.waitForTimeout(1500)
|
|
const m = await p.evaluate(() => {
|
|
const de = document.documentElement
|
|
return {
|
|
scrollW: de.scrollWidth,
|
|
clientW: de.clientWidth,
|
|
overflowers: [...document.querySelectorAll('body *')]
|
|
.filter((el) => el.scrollWidth > el.clientWidth + 1 && getComputedStyle(el).overflowX === 'visible')
|
|
.slice(0, 4)
|
|
.map((el) => `${el.tagName}.${(el.className || '').toString().slice(0, 40)}`),
|
|
}
|
|
})
|
|
const hOverflow = m.scrollW > m.clientW + 1
|
|
ok(
|
|
`[${role}] ${vp.w}px 无整页横向滚动`,
|
|
!hOverflow,
|
|
`scrollW=${m.scrollW} clientW=${m.clientW}${m.overflowers.length ? ' overflowers=' + JSON.stringify(m.overflowers) : ''}`,
|
|
)
|
|
if (vp.w === 320) await p.screenshot({ path: `${SHOT_DIR}/vp320-${role}.png`, fullPage: true })
|
|
}
|
|
await ctx.close()
|
|
}
|
|
|
|
// ---------------- keyboard focus visibility ----------------
|
|
{
|
|
const ctx = await b.newContext({ viewport: { width: 1280, height: 900 } })
|
|
const p = await ctx.newPage()
|
|
await p.goto(`${BASE}/#/login`, { waitUntil: 'domcontentloaded' })
|
|
await p.waitForTimeout(2500)
|
|
const rings = []
|
|
for (let i = 0; i < 6; i++) {
|
|
await p.keyboard.press('Tab')
|
|
await p.waitForTimeout(200)
|
|
const info = await p.evaluate(() => {
|
|
const el = document.activeElement
|
|
if (!el || el === document.body) return null
|
|
const cs = getComputedStyle(el)
|
|
return {
|
|
tag: el.tagName,
|
|
text: (el.innerText || el.getAttribute('aria-label') || '').trim().slice(0, 24),
|
|
outline: `${cs.outlineStyle} ${cs.outlineWidth} ${cs.outlineColor}`,
|
|
boxShadow: cs.boxShadow.slice(0, 60),
|
|
}
|
|
})
|
|
if (info) rings.push(info)
|
|
}
|
|
const withRing = rings.filter(
|
|
(r) => (r.outline.includes('solid') && !r.outline.includes('0px')) || (r.boxShadow && r.boxShadow !== 'none'),
|
|
)
|
|
ok('键盘 Tab 有可见焦点环', withRing.length >= Math.min(3, rings.length), JSON.stringify(rings.slice(0, 4)))
|
|
await ctx.close()
|
|
}
|
|
|
|
// F-13:明细已在产生时即时输出;此处只给汇总(即使中断也不丢已完成证据)
|
|
const _nP = out.filter((l) => l.startsWith('PASS')).length
|
|
const _nF = out.filter((l) => l.startsWith('FAIL')).length
|
|
const _nI = out.filter((l) => l.startsWith('INFO')).length
|
|
console.log(`
|
|
===== ${_nP} PASS / ${_nF} FAIL / ${_nI} INFO =====`)
|
|
await b.close()
|