mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 06:41:58 +00:00
对比页与触发历史照实说「上一次成功的运行」:两个同名入口打开同一对运行、写同一句来历
服务端的默认基准和自动对比已经共用一个挑法(只挑比本次先发起、成功结束并封存的运行),界面上的说法跟着改: - 对比页的「默认基准:…」改成「同一定时任务上一次成功的运行」「同一工作流、同一运行类别上一次成功的运行」;运行详情 查找基准时的提示、没带本次时的说明同样改成「上一次成功的运行」 - 触发历史的「与上一次对比」:基准不是紧挨着的上一次(previous 为 false)时,链接上看得见的字写「与上一次成功的运行 对比」,触屏、键盘聚焦都看得到,悬停照旧写是哪一次。两种都带上「同一定时任务」的来历:新的说法对这两种都成立, 对比页和从运行详情进去时一字不差 - check-compare:两种来历的说法;运行详情「更多」和触发历史两个入口打开同一对运行、写同一句来历、摘要一致 - check-schedules:previous 为 false 时看得见的字(含键盘聚焦、390px 窄屏);两个入口拿同一个基准
This commit is contained in:
@@ -3085,10 +3085,15 @@ export const SCHEDULE_TEXT = {
|
||||
|
||||
// ---- 触发历史里的对比:运行成功结束后,服务端自动和同一定时任务上一次成功的运行比一次
|
||||
/**
|
||||
* 和运行详情「更多」菜单里的入口是同一件事,用同一个叫法。RUN_COMPARE_TEXT 定义在本对象之后,模块初始化时还取不到,
|
||||
* 写成 getter 用到时再取
|
||||
* 和运行详情「更多」菜单里的入口是同一件事(服务端用同一个函数挑基准,两处比的是同一对运行),用同一个叫法。
|
||||
* RUN_COMPARE_TEXT 定义在本对象之后,模块初始化时还取不到,写成 getter 用到时再取
|
||||
*/
|
||||
get compareLink() { return RUN_COMPARE_TEXT.menu },
|
||||
/**
|
||||
* 基准不是紧挨着的上一次(上一次失败了、取消了,比的是再往前成功的那一次)时链接上看得见的叫法:触屏没有悬停,
|
||||
* 键盘聚焦时也不显示 title,只在悬停里说明会让人以为比的就是上一次
|
||||
*/
|
||||
compareLinkLastOk: '与上一次成功的运行对比',
|
||||
/**
|
||||
* 悬停写各类指标的个数,和 Webhook 通知里那一行同一个写法(服务端 scheduler/auto_compare.py 的 summary_text):
|
||||
* 变化三分法的三类总写上,0 个也写;其余各类有才写,次序同对比页;两边都没有指标时不写一串 0 个。只有个数,
|
||||
@@ -3160,7 +3165,7 @@ export const RUN_COMPARE_TEXT = {
|
||||
menuSealedOnly: '运行结束并封存后才能对比',
|
||||
/** 没有 workflow_id 的运行:画布、问数据里没保存就运行的,或者工作流删掉之后被置空的。服务端只比同一个已保存的工作流 */
|
||||
menuNoWorkflow: '工作流未保存或已删除,无法对比',
|
||||
menuFinding: '正在查找上一次运行…',
|
||||
menuFinding: '正在查找上一次成功的运行…',
|
||||
pickManually: '手动选择基准',
|
||||
retry: '重试',
|
||||
// 顶部的两次运行
|
||||
@@ -3175,9 +3180,13 @@ export const RUN_COMPARE_TEXT = {
|
||||
runFailed: '无法读取这次运行',
|
||||
runMissing: '运行记录不存在',
|
||||
noBase: '未选择',
|
||||
/**
|
||||
* 默认基准是怎么选的,照服务端(api/compare.py 的 pick_base)的规则写:只挑比本次先发起、成功结束并封存的运行。
|
||||
* 上一次失败、取消了时基准是再往前成功的那一次,所以不能写成「上一次运行」
|
||||
*/
|
||||
reason: {
|
||||
same_schedule: '默认基准:同一定时任务的上一次运行',
|
||||
same_workflow: '默认基准:同一工作流、同一运行类别的上一次运行',
|
||||
same_schedule: '默认基准:同一定时任务上一次成功的运行',
|
||||
same_workflow: '默认基准:同一工作流、同一运行类别上一次成功的运行',
|
||||
} as Record<string, string>,
|
||||
pickBase: '更换基准',
|
||||
pickHint: '同一工作流最近的运行,未封存的不能作为基准',
|
||||
@@ -3195,7 +3204,7 @@ export const RUN_COMPARE_TEXT = {
|
||||
pickGone: (id: string, missing: boolean) => `${id}(${missing ? '记录不存在' : '无法读取'})`,
|
||||
// 页面状态
|
||||
noTarget: '未指定要对比的运行',
|
||||
noTargetHint: '在运行详情的「更多」菜单中选择「与上一次对比」,即可对照本次与上一次运行',
|
||||
noTargetHint: '在运行详情的「更多」菜单中选择「与上一次对比」,即可对照本次与上一次成功的运行',
|
||||
toRuns: '前往记录',
|
||||
finding: '正在查找基准运行…',
|
||||
loading: '正在对比两次运行…',
|
||||
|
||||
@@ -17,8 +17,9 @@ export const comparePath = (target: string, base?: string | null) =>
|
||||
const NOTE_DELAY_MS = 300
|
||||
|
||||
/**
|
||||
* 运行详情「更多」菜单里的「与上一次对比」。先问服务端默认的基准(同一定时任务的上一次,没有就同一工作流、同一类别的
|
||||
* 上一次),找到了再跳到对比页;找不到时留在原页,提示服务端给的原因,并给「手动选择基准」去对比页自己挑。
|
||||
* 运行详情「更多」菜单里的「与上一次对比」。先问服务端默认的基准(同一定时任务上一次成功的运行,没有就同一工作流、
|
||||
* 同一类别上一次成功的运行;定时任务触发历史里同名的入口由服务端用同一个函数挑,两处比的是同一对运行),找到了再跳到
|
||||
* 对比页;找不到时留在原页,提示服务端给的原因,并给「手动选择基准」去对比页自己挑。
|
||||
* 这几种不给点,点了也只会被拒:没封存、还在运行的(对比只认已封存的运行,409);不属于已保存工作流的——画布、问数据
|
||||
* 里没保存就运行的,和工作流删掉之后 workflow_id 被置空的(服务端找不到同一工作流的运行,手动选也是 422)。
|
||||
*
|
||||
|
||||
@@ -14,7 +14,8 @@ const PAGE = 20
|
||||
/**
|
||||
* 触发历史:每次触发一行,新的在前,20 条一页、「加载更多」往后接。计划时间按定时任务的时区写;跳过、发起失败
|
||||
* 写原因;发起了的链到那次运行、写运行现在的状态;运行成功结束后服务端和同一任务上一次成功的运行比过的,另给
|
||||
* 「与上一次对比」;发出去的通知逐条写结果。refreshKey 变了(立即运行之后)从头重取
|
||||
* 「与上一次对比」(基准不是紧挨着的上一次时写「与上一次成功的运行对比」);发出去的通知逐条写结果。refreshKey 变了
|
||||
* (立即运行之后)从头重取
|
||||
*/
|
||||
export function FireHistory({ schedule, refreshKey }: { schedule: Schedule; refreshKey: number }) {
|
||||
const [items, setItems] = useState<ScheduleFire[] | null>(null)
|
||||
@@ -142,14 +143,17 @@ function FireRow({ fire: f, tz, now }: { fire: ScheduleFire; tz: string; now: nu
|
||||
}
|
||||
|
||||
/**
|
||||
* 「与上一次对比」:去对比页,本次是这次触发的运行、基准是服务端自动对比时取的同一任务上一次成功的运行。旁边写有几个
|
||||
* 指标有变化(触屏没有悬停,键盘聚焦也不显示悬停),悬停写各类指标的个数(和通知里那一行同一个写法),读屏把它当
|
||||
* 说明念。基准就是紧挨着的上一次时带上来历,对比页据此写「默认基准:同一定时任务的上一次运行」;不是(上一次失败了、
|
||||
* 取消了)时不带,免得说错,悬停写明比的是哪一次
|
||||
* 「与上一次对比」:去对比页,本次是这次触发的运行、基准是服务端自动对比时取的同一任务上一次成功的运行——和运行详情
|
||||
* 「更多」里同名入口的默认基准是同一个挑法(服务端 api/compare.py 的 pick_base),两处进去比的是同一对运行。旁边写有
|
||||
* 几个指标有变化(触屏没有悬停,键盘聚焦也不显示悬停),悬停写各类指标的个数(和通知里那一行同一个写法),读屏把它
|
||||
* 当说明念。
|
||||
*
|
||||
* 基准不是紧挨着的上一次(上一次失败了、取消了)时,链接上看得见的字就写「与上一次成功的运行对比」,悬停另写是哪一次。
|
||||
* 两种都带上来历:对比页据此写「默认基准:同一定时任务上一次成功的运行」,和从运行详情进去时一字不差
|
||||
*/
|
||||
function CompareLink({ runId, compare }: { runId: string; compare: ScheduleFireCompare }) {
|
||||
const previous = compare.previous !== false
|
||||
const via: CompareVia | undefined = previous ? { base: compare.base_run_id, reason: 'same_schedule' } : undefined
|
||||
const via: CompareVia = { base: compare.base_run_id, reason: 'same_schedule' }
|
||||
const count = T.compareCount(compare.summary)
|
||||
return (
|
||||
<Link
|
||||
@@ -157,10 +161,11 @@ function CompareLink({ runId, compare }: { runId: string; compare: ScheduleFireC
|
||||
state={via}
|
||||
title={T.compareTitle(compare.summary, previous ? null : shortId(compare.base_run_id))}
|
||||
data-fire-compare={compare.base_run_id}
|
||||
data-fire-compare-previous={previous ? '1' : '0'}
|
||||
className="inline-flex shrink-0 items-center gap-1 rounded-sm text-2xs text-dim outline-none hover:text-fg hover:underline focus-visible:ring-2 focus-visible:ring-[var(--accent)]"
|
||||
>
|
||||
<GitCompareArrows size={11} aria-hidden />
|
||||
<span data-fire-compare-label="">{T.compareLink}</span>
|
||||
<span data-fire-compare-label="">{previous ? T.compareLink : T.compareLinkLastOk}</span>
|
||||
{count && (
|
||||
<>
|
||||
<span aria-hidden>·</span>
|
||||
|
||||
@@ -9,10 +9,13 @@
|
||||
// 同一节点两条查询的 (node_id, ordinal) 相同;一边的 SQL 是 null;基准运行读不到;本次的记录读不到;不属于已保存
|
||||
// 工作流的运行;查找基准期间离开运行详情(新页面画好之后、或者地址刚换新页面还没画出来时答复才回来,走过的地址里
|
||||
// 都不能出现对比页)。下一步的提示要和页面上真能做的事对得上:下拉里没有能换的运行就不叫人去上方换,本次的记录
|
||||
// 不存在时不给注定失败的「重试」。
|
||||
// 不存在时不给注定失败的「重试」。默认基准的说法照服务端的挑法(api/compare.py 的 pick_base)写「上一次成功的运行」
|
||||
// ——上一次失败了,基准是再往前成功的那一次;定时任务触发历史里的「与上一次对比」和运行详情里的同名入口打开同一对
|
||||
// 运行、写同一句来历,基准不是紧挨着的上一次时触发历史上看得见的字写明「上一次成功的运行」。
|
||||
//
|
||||
// 全部用 page.route 假接口:虚构的「区域销售周报」三次已封存的正式运行和一次没封存的,运行详情、运行列表、
|
||||
// compare/base、compare、定时任务列表都在浏览器层答掉,别的 GET 放行(外壳要的工作流、待审批),非 GET 一律拦掉。
|
||||
// compare/base、compare、定时任务列表、发起它们的定时任务和触发历史都在浏览器层答掉,别的 GET 放行(外壳要的工作流、
|
||||
// 待审批),非 GET 一律拦掉。
|
||||
// 运行对比的两个接口只读,这里的假答复严格照接口约定的字段名和形状。
|
||||
//
|
||||
// node scripts/check-compare.mjs
|
||||
@@ -181,11 +184,31 @@ function comparison(target, base) {
|
||||
const LABEL = { unchanged: '未变', data_only: '只数据变', caliber_and_data: '口径与数据都变', sql_changed: 'SQL 结构变',
|
||||
params_only: '只参数变', added: '新增', removed: '移除', incomparable: '无法比较' }
|
||||
const ORDER = ['data_only', 'caliber_and_data', 'sql_changed', 'params_only', 'added', 'removed', 'incomparable', 'unchanged']
|
||||
const REASON = '默认基准:同一定时任务的上一次运行'
|
||||
const FINDING = '正在查找上一次运行'
|
||||
// 默认基准只挑比本次先发起、成功结束并封存的运行(服务端 api/compare.py 的 pick_base):说法里都是「上一次成功的运行」
|
||||
const REASON = '默认基准:同一定时任务上一次成功的运行'
|
||||
const REASON_WORKFLOW = '默认基准:同一工作流、同一运行类别上一次成功的运行'
|
||||
const FINDING = '正在查找上一次成功的运行'
|
||||
/** 409、422、缺基准的 404 时下拉里没有能换的运行:不叫人去上方换,照实说 */
|
||||
const NO_OTHER = '同一工作流没有其他可换作基准的已封存运行'
|
||||
|
||||
/**
|
||||
* 发起本次和基准的定时任务(RUNS 里的 schedule_id)和它的触发历史:本次那一行带服务端自动对比的计数,基准是 B、不是
|
||||
* 紧挨着的上一次(previous: false,假定 B 之后同一定时任务还有一次失败的;那一次不进 RUNS,换基准的下拉照旧)。
|
||||
* 只给「两个入口拿同一个基准」那一段用:触发历史和运行详情「更多」里的同名入口,服务端用同一个函数挑基准
|
||||
*/
|
||||
const SCHEDULE_ID = 'sch0weekly'
|
||||
const SCHEDULE = {
|
||||
id: SCHEDULE_ID, name: '区域销售周报 · 每周', workflow_id: WF, workflow_name: NAME, cron: '0 9 * * 1', timezone: 'Asia/Shanghai',
|
||||
run_class: 'formal', input_template: {}, enabled: true, overlap: 'skip', misfire_grace_s: 3600, webhook: null,
|
||||
next_run_at: '2026-10-12T01:00:00Z', last_fire: null, description_text: '每周一 09:00(Asia/Shanghai)', created_by: '值班分析员',
|
||||
created_at: day('09-01'), updated_at: day('09-01'), version: 2,
|
||||
}
|
||||
const FIRE_T = 'fire0target'
|
||||
const FIRES = [{
|
||||
id: FIRE_T, fire_at: day('10-05'), kind: 'scheduled', status: 'started', run_id: T, run_status: 'succeeded', reason: null,
|
||||
notified: [], created_at: day('10-05'), compare: { base_run_id: B, previous: false, summary: SUMMARY },
|
||||
}]
|
||||
|
||||
// ---------------------------------------------------------------- 假接口
|
||||
|
||||
function events() {
|
||||
@@ -247,6 +270,9 @@ function defaults(opts) {
|
||||
if (m[2] === '/artifacts') return { json: [] }
|
||||
}
|
||||
if (p === '/api/approvals' && url.searchParams.get('run_id')) return { json: [] }
|
||||
// 发起本次的定时任务和它的触发历史(只有直接打开那个定时任务的页面时才用到)
|
||||
if (p === `/api/schedules/${SCHEDULE_ID}`) return { json: SCHEDULE }
|
||||
if (p === `/api/schedules/${SCHEDULE_ID}/fires`) return { json: { items: FIRES, next_before: null } }
|
||||
// 查找基准期间离开运行详情去的那一页(定时任务列表):给空的列表,页面落定不靠服务端里有什么
|
||||
if (p === '/api/schedules') return { json: { items: [] } }
|
||||
return null
|
||||
@@ -397,7 +423,7 @@ async function findingBase(answer = { json: { base_run_id: B, reason: 'same_sche
|
||||
|
||||
await section('查找基准期间:看得见在做事,离开之后不再跳走', async () => {
|
||||
const { page, release, ended, noted, visited, errors } = await findingBase()
|
||||
check('点了之后有「正在查找上一次运行」的提示', noted)
|
||||
check('点了之后有「正在查找上一次成功的运行」的提示', noted)
|
||||
// 改主意去了别的页面。等那一页画好、运行详情卸下之后再放行:答复晚到也不能把人拽回对比页
|
||||
await page.evaluate((to) => { history.pushState({}, '', to); dispatchEvent(new PopStateEvent('popstate')) }, AWAY)
|
||||
await awaySettled(page)
|
||||
@@ -476,6 +502,60 @@ await section('未封存的运行:入口不可点并说明原因', async () =>
|
||||
}
|
||||
})
|
||||
|
||||
/** 在运行详情里点「更多」→「与上一次对比」,等对比页落定 */
|
||||
async function viaMenu(page) {
|
||||
await page.locator(`[data-run-detail="${T}"]`).waitFor({ timeout: 10000 })
|
||||
await page.getByRole('button', { name: '更多操作' }).click()
|
||||
await page.locator('[data-menu=compare]').click()
|
||||
await page.waitForURL((u) => u.pathname === '/runs/compare', { timeout: 8000 })
|
||||
await settled(page)
|
||||
}
|
||||
|
||||
await section('默认基准的说法:上一次成功的运行', async () => {
|
||||
// 服务端只挑比本次先发起、成功结束并封存的运行:上一次失败、取消了时基准是再往前成功的那一次,「上一次运行」是错的
|
||||
for (const [reason, want] of [['same_schedule', REASON], ['same_workflow', REASON_WORKFLOW]]) {
|
||||
const base = () => ({ json: { base_run_id: B, reason, base: refOf(B) } })
|
||||
const menu = await open(`/runs/${T}`, { base })
|
||||
await viaMenu(menu.page)
|
||||
const fromMenu = (await maybeText(menu.page.locator('[data-compare-reason]'))).trim()
|
||||
const self = await open(comparePath(T), { base })
|
||||
await settled(self.page)
|
||||
const fromPage = (await maybeText(self.page.locator('[data-compare-reason]'))).trim()
|
||||
check(`${reason}:从运行详情进入、只带本次打开,都写「${want}」`, fromMenu === want && fromPage === want, `${fromMenu} | ${fromPage}`)
|
||||
const pages = [await menu.page.locator('[data-compare-page]').innerText(), await self.page.locator('[data-compare-page]').innerText()]
|
||||
check(`${reason}:页面上不出现「上一次运行」这类不分成败的说法`, pages.every((t) => !t.includes('上一次运行')))
|
||||
check(`${reason}:页面没有报错`, ![...menu.errors, ...self.errors].length, [...menu.errors, ...self.errors].join(';'))
|
||||
for (const c of opened) await c.close().catch(() => {})
|
||||
opened.clear()
|
||||
}
|
||||
})
|
||||
|
||||
await section('运行详情与触发历史的「与上一次对比」:同一个基准、同一句来历', async () => {
|
||||
// 两处的基准由服务端用同一个函数挑(api/compare.py 的 pick_base):上一次失败了,两处都是再往前成功的 B
|
||||
const menu = await open(`/runs/${T}`)
|
||||
await viaMenu(menu.page)
|
||||
const hist = await open(`/schedules/${SCHEDULE_ID}?tab=history`)
|
||||
const link = hist.page.locator(`[data-fire-id="${FIRE_T}"] [data-fire-compare]`)
|
||||
await link.waitFor({ timeout: 10000 })
|
||||
const label = (await link.locator('[data-fire-compare-label]').innerText()).trim()
|
||||
check('触发历史:基准不是紧挨着的上一次,链接上看得见的字写「与上一次成功的运行对比」', label === '与上一次成功的运行对比', label)
|
||||
await link.click()
|
||||
await hist.page.waitForURL((u) => u.pathname === '/runs/compare', { timeout: 8000 })
|
||||
await settled(hist.page)
|
||||
const [a, b] = [new URL(menu.page.url()), new URL(hist.page.url())]
|
||||
check('两个入口打开的是同一对运行(本次和基准都一样)', a.searchParams.get('target') === T && b.searchParams.get('target') === T
|
||||
&& a.searchParams.get('base') === B && b.searchParams.get('base') === B, `${a.search} | ${b.search}`)
|
||||
const [ra, rb] = [(await maybeText(menu.page.locator('[data-compare-reason]'))).trim(),
|
||||
(await maybeText(hist.page.locator('[data-compare-reason]'))).trim()]
|
||||
check('两个入口写的默认基准一字不差:同一定时任务上一次成功的运行', ra === REASON && rb === REASON, `${ra} | ${rb}`)
|
||||
const tiles = (page) => page.locator('[data-compare-summary] [data-compare-class]').evaluateAll((els) =>
|
||||
els.map((e) => `${e.getAttribute('data-compare-class')}=${e.querySelector('[data-compare-count]')?.textContent?.trim()}`).join(' '))
|
||||
const [ta, tb] = [await tiles(menu.page), await tiles(hist.page)]
|
||||
check('两边的变化摘要一致', !!ta && ta === tb, `${ta} | ${tb}`)
|
||||
check('没有发出写请求', ![...menu.blocked, ...hist.blocked].length, [...menu.blocked, ...hist.blocked].join(';'))
|
||||
check('页面没有报错', ![...menu.errors, ...hist.errors].length, [...menu.errors, ...hist.errors].join(';'))
|
||||
})
|
||||
|
||||
await section('两次运行与摘要', async () => {
|
||||
const { page, errors } = await open(comparePath(T, B))
|
||||
await settled(page)
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
// 立即运行之后没有下文、触发历史只有一页、保存过的 Webhook 地址又被原样显示出来、改了别的字段却把地址清掉、
|
||||
// 记录页看不出哪些运行来自定时任务、390px 窄屏被表单撑出横向滚动。运行成功结束后的自动对比:触发历史里「与上一次
|
||||
// 对比」(和运行详情「更多」菜单同一个叫法)链到对比页,旁边写有几个指标有变化,悬停只写各类指标的个数,基准不是
|
||||
// 紧挨着的上一次时写明是哪一次、不带「默认基准」的来历;运行详情页头的「由定时任务「X」发起」是键盘到得了的真链接
|
||||
// (记录页的「定时」标记在整行按钮里,只能鼠标点),带日历图标,窄窗口里(768、820、900 宽,探索运行多一个「提取为
|
||||
// 草稿」)不被挤没,定时任务删掉之后写成纯文本。
|
||||
// 紧挨着的上一次时链接上看得见的字写「与上一次成功的运行对比」、悬停写明是哪一次;两种都带「默认基准:同一定时任务
|
||||
// 上一次成功的运行」的来历,和运行详情「更多」里的同名入口打开同一对运行(服务端用同一个函数挑基准)。运行详情页头的
|
||||
// 「由定时任务「X」发起」是键盘到得了的真链接(记录页的「定时」标记在整行按钮里,只能鼠标点),带日历图标,窄窗口里
|
||||
// (768、820、900 宽,探索运行多一个「提取为草稿」)不被挤没,定时任务删掉之后写成纯文本。
|
||||
// 还有几处时序和口径:晚到的刷新把刚切换的开关盖回去、先开触发历史再开设置时带着旧版本撞假的 409、保存途中又改了
|
||||
// 别的字段时替换过的地址明文留在框里、服务端会拒的组合(未发布 + 正式运行、必填没填、已发布版本里没有的字段)
|
||||
// 照样放行、测试消息的结果把服务端的整句又套一层、错过已跳过的那条悬停另说一个原因(和本条记的原因对不上)。
|
||||
@@ -267,9 +268,15 @@ await section('纯函数:服务端会拒的组合、保存回来怎么接、
|
||||
check(`测试结果:服务端给整句、给补充说明都只说一遍(${tests.length} 种)`, !wrong.length, wrong.map(([r]) => t(r)).join(';'))
|
||||
})
|
||||
|
||||
/** 对比页写的默认基准来历(定时任务发起的):只挑比本次先发起、成功结束并封存的运行,服务端 api/compare.py 的 pick_base */
|
||||
const REASON_SCHEDULE = '默认基准:同一定时任务上一次成功的运行'
|
||||
|
||||
await section('纯函数:触发历史对比的文案', async () => {
|
||||
const { SCHEDULE_TEXT: T, RUN_COMPARE_TEXT: C } = await import(await load('/src/lib/terms.ts'))
|
||||
check('「与上一次对比」和运行详情「更多」菜单同一个叫法', T.compareLink === C.menu && T.compareLink === '与上一次对比', T.compareLink)
|
||||
check('基准不是紧挨着的上一次时的叫法:与上一次成功的运行对比', T.compareLinkLastOk === '与上一次成功的运行对比', T.compareLinkLastOk)
|
||||
check('默认基准的来历照服务端的挑法写「上一次成功的运行」', C.reason.same_schedule === REASON_SCHEDULE
|
||||
&& C.reason.same_workflow === '默认基准:同一工作流、同一运行类别上一次成功的运行', JSON.stringify(C.reason))
|
||||
const zero = { unchanged: 0, data_only: 0, caliber_and_data: 0, sql_changed: 0, params_only: 0, added: 0, removed: 0, incomparable: 0 }
|
||||
const cases = [
|
||||
['有变化的几类合起来', { ...zero, data_only: 3, caliber_and_data: 1, params_only: 2, unchanged: 4 }, '6 个指标有变化'],
|
||||
@@ -1462,6 +1469,11 @@ await section('详情:触发历史里的「与上一次对比」', async () =>
|
||||
const f10 = row('f10').locator('[data-fire-compare]')
|
||||
const t10 = (await f10.getAttribute('title').catch(() => '')) ?? ''
|
||||
check('上一次失败了、基准是再往前成功的那次:悬停写明是哪一次', t10.startsWith('与上一次成功的运行(#run-f1)相比:只数据变 1 个'), t10)
|
||||
// 触屏没有悬停、键盘聚焦也不显示 title:比的不是上一次这件事得写在看得见的字里
|
||||
const l10 = f10.locator('[data-fire-compare-label]')
|
||||
check('……链接上看得见的字写「与上一次成功的运行对比」,不只靠悬停', (await l10.innerText().catch(() => '')).trim() === '与上一次成功的运行对比'
|
||||
&& await l10.isVisible(), (await l10.innerText().catch(() => '没有')).trim())
|
||||
check('……紧挨着的上一次那一行照旧写「与上一次对比」', (await row('f00').locator('[data-fire-compare-label]').innerText()).trim() === '与上一次对比')
|
||||
const linked = await page.locator('[data-fire-compare]').evaluateAll((els) => els.map((e) => e.closest('[data-fire-id]')?.getAttribute('data-fire-id')))
|
||||
check('没有对比的(compare 为 null、老服务端不给、没有运行)不给链接', JSON.stringify(linked) === '["f00","f08","f10"]', JSON.stringify(linked))
|
||||
check('运行链接照旧在', await row('f00').locator('a[href="/runs/run-f00"]').count() === 1)
|
||||
@@ -1475,21 +1487,77 @@ await section('详情:触发历史里的「与上一次对比」', async () =>
|
||||
})
|
||||
check('……回车打开对比页,带着本次和基准', !!url && url.searchParams.get('target') === 'run-f00'
|
||||
&& url.searchParams.get('base') === 'run-f02', page.url())
|
||||
// 来历跟着这一次跳转走(路由的 state),对比页据此写「默认基准:同一定时任务的上一次运行」
|
||||
// 来历跟着这一次跳转走(路由的 state),对比页据此写「默认基准:同一定时任务上一次成功的运行」
|
||||
const via = await page.evaluate(() => history.state?.usr ?? null)
|
||||
check('……紧挨着的上一次:带上「同一定时任务」的来历', via?.reason === 'same_schedule' && via?.base === 'run-f02', JSON.stringify(via))
|
||||
|
||||
// 基准不是紧挨着的上一次:来历照样带上。对比页写的是「上一次成功的运行」,比的正是这一次,和从运行详情进去一字不差
|
||||
const second = await open('/schedules/s-weekly?tab=history')
|
||||
await second.page.locator('[data-fire-id="f10"] [data-fire-compare]').click()
|
||||
await second.page.locator('[data-fire-id="f10"] [data-fire-compare]').focus()
|
||||
const focusedLabel = await second.page.evaluate(() => document.activeElement?.querySelector('[data-fire-compare-label]')?.textContent?.trim() ?? '')
|
||||
check('……键盘聚焦在那一行的链接上时,看得见的字就是「与上一次成功的运行对比」', focusedLabel === '与上一次成功的运行对比', focusedLabel)
|
||||
await second.page.keyboard.press('Enter')
|
||||
await until(() => new URL(second.page.url()).pathname === '/runs/compare')
|
||||
const via10 = await second.page.evaluate(() => history.state?.usr ?? null)
|
||||
check('……基准不是紧挨着的上一次:不带来历,对比页不说「上一次」', new URL(second.page.url()).searchParams.get('base') === 'run-f12'
|
||||
&& via10 == null, JSON.stringify(via10))
|
||||
const reason10 = await until(async () => (await second.page.locator('[data-compare-reason]').innerText().catch(() => '')).trim())
|
||||
check('……基准不是紧挨着的上一次:带上来历,对比页写「同一定时任务上一次成功的运行」', new URL(second.page.url()).searchParams.get('base') === 'run-f12'
|
||||
&& via10?.reason === 'same_schedule' && via10?.base === 'run-f12' && reason10 === REASON_SCHEDULE, `${JSON.stringify(via10)} ${reason10}`)
|
||||
const real = [...writes(sent, /./), ...writes(second.sent, /./)].filter((x) => x.key !== 'POST /schedules/preview')
|
||||
check('只读:没有写请求', real.length === 0, real.map((x) => x.key).join('、'))
|
||||
check('没有页面报错', ![...errors, ...second.errors].length, [...errors, ...second.errors].join(' | '))
|
||||
})
|
||||
|
||||
await section('详情:触发历史和运行详情的「与上一次对比」拿同一个基准', async () => {
|
||||
// f10 的上一次(f11)失败了,服务端两处都挑再往前成功的 f12:自动对比和运行详情的默认基准用的是同一个函数
|
||||
// (api/compare.py 的 pick_base)。这里守前端:两个入口都原样用服务端给的基准,打开同一对运行、写同一句来历
|
||||
const runOf = (id, at) => ({ ...RUNS[0], id, started_by: '定时任务:门店周报 · 每周', created_at: iso(at), started_at: iso(at),
|
||||
finished_at: iso(at + 4200), manifest_hash: 'ab'.repeat(32), manifest_seq: 2 })
|
||||
const f10 = runOf('run-f10', nextMonday9(-11))
|
||||
const f12 = runOf('run-f12', nextMonday9(-13))
|
||||
const ref = (r) => ({ id: r.id, workflow_name: r.workflow_name, version: r.version, run_class: r.run_class,
|
||||
started_at: r.started_at, finished_at: r.finished_at, schedule_id: r.schedule_id })
|
||||
const compareApi = (method, path, body, url) => {
|
||||
if (method !== 'GET') return null
|
||||
if (path === '/runs/run-f10/compare/base') return { json: { base_run_id: 'run-f12', reason: 'same_schedule', base: ref(f12) } }
|
||||
if (path === '/runs/run-f10/compare' && url.searchParams.get('base') === 'run-f12') {
|
||||
return { json: { target: ref(f10), base: ref(f12), summary: COMPARE_F10.summary, metrics: [], queries: [], notes: [],
|
||||
template: { base_version: 3, target_version: 3, same_graph: true, nodes: [] } } }
|
||||
}
|
||||
return null
|
||||
}
|
||||
const stateOf = () => ({ ...freshState({ runs: [...RUNS, f10, f12] }), override: compareApi })
|
||||
const landed = (page) => until(() => {
|
||||
const u = new URL(page.url())
|
||||
return u.pathname === '/runs/compare' ? u : null
|
||||
})
|
||||
const reasonOf = (page) => until(async () => (await page.locator('[data-compare-reason]').innerText().catch(() => '')).trim())
|
||||
|
||||
const menu = await open('/runs/run-f10', { state: stateOf() })
|
||||
await menu.page.locator('[data-run-detail="run-f10"]').waitFor({ timeout: 10000 })
|
||||
await menu.page.getByRole('button', { name: '更多操作' }).click()
|
||||
await menu.page.locator('[data-menu=compare]').click()
|
||||
const a = await landed(menu.page)
|
||||
const hist = await open('/schedules/s-weekly?tab=history', { state: stateOf() })
|
||||
const link = hist.page.locator('[data-fire-id="f10"] [data-fire-compare]')
|
||||
await link.waitFor()
|
||||
const label = (await link.locator('[data-fire-compare-label]').innerText()).trim()
|
||||
await link.click()
|
||||
const b = await landed(hist.page)
|
||||
check('运行详情「更多」和触发历史两个入口:打开同一对运行(本次 run-f10,基准是跳过失败那次的 run-f12)',
|
||||
!!a && !!b && a.searchParams.get('target') === 'run-f10' && b.searchParams.get('target') === 'run-f10'
|
||||
&& a.searchParams.get('base') === 'run-f12' && b.searchParams.get('base') === 'run-f12', `${a?.search} | ${b?.search}`)
|
||||
const [ra, rb] = [await reasonOf(menu.page), await reasonOf(hist.page)]
|
||||
check('……两处写的默认基准一字不差:同一定时任务上一次成功的运行', ra === REASON_SCHEDULE && rb === REASON_SCHEDULE, `${ra} | ${rb}`)
|
||||
check('……触发历史上看得见的字说明比的是上一次成功的运行', label === '与上一次成功的运行对比', label)
|
||||
const tiles = (page) => until(async () => (await page.locator('[data-compare-summary] [data-compare-class]').evaluateAll((els) =>
|
||||
els.map((e) => `${e.getAttribute('data-compare-class')}=${e.querySelector('[data-compare-count]')?.textContent?.trim()}`))).join(' '))
|
||||
const [ta, tb] = [await tiles(menu.page), await tiles(hist.page)]
|
||||
check('……两边的变化摘要一致', !!ta && ta === tb, `${ta} | ${tb}`)
|
||||
const real = [...writes(menu.sent, /./), ...writes(hist.sent, /./)]
|
||||
check('只读:没有写请求', real.length === 0, real.map((x) => x.key).join('、'))
|
||||
check('没有页面报错', ![...menu.errors, ...hist.errors].length, [...menu.errors, ...hist.errors].join(' | '))
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- 运行详情
|
||||
|
||||
/**
|
||||
@@ -1739,6 +1807,8 @@ await section('窄屏 390px 与浅色', async () => {
|
||||
check(`${theme}:触发历史不横向溢出`, await overflow(hist.page) <= 1, `${await overflow(hist.page)}px`)
|
||||
check(`${theme}:「与上一次对比」和旁边的计数在屏幕里`, await inViewX(hist.page, '[data-fire-id="f00"] [data-fire-compare]')
|
||||
&& await inViewX(hist.page, '[data-fire-id="f00"] [data-fire-compare-count]'))
|
||||
check(`${theme}:更长的「与上一次成功的运行对比」连同计数也在屏幕里`, await inViewX(hist.page, '[data-fire-id="f10"] [data-fire-compare]')
|
||||
&& await inViewX(hist.page, '[data-fire-id="f10"] [data-fire-compare-count]'))
|
||||
const cmp = await readable(hist.page, '[data-fire-id="f00"] [data-fire-compare]')
|
||||
check(`${theme}:「与上一次对比」对比度 ≥ 4.5`, !!cmp && cmp.ratio >= 4.5, JSON.stringify(cmp))
|
||||
const cnt = await readable(hist.page, '[data-fire-id="f00"] [data-fire-compare-count]')
|
||||
|
||||
Reference in New Issue
Block a user