对比页与触发历史照实说「上一次成功的运行」:两个同名入口打开同一对运行、写同一句来历

服务端的默认基准和自动对比已经共用一个挑法(只挑比本次先发起、成功结束并封存的运行),界面上的说法跟着改:

- 对比页的「默认基准:…」改成「同一定时任务上一次成功的运行」「同一工作流、同一运行类别上一次成功的运行」;运行详情
  查找基准时的提示、没带本次时的说明同样改成「上一次成功的运行」
- 触发历史的「与上一次对比」:基准不是紧挨着的上一次(previous 为 false)时,链接上看得见的字写「与上一次成功的运行
  对比」,触屏、键盘聚焦都看得到,悬停照旧写是哪一次。两种都带上「同一定时任务」的来历:新的说法对这两种都成立,
  对比页和从运行详情进去时一字不差
- check-compare:两种来历的说法;运行详情「更多」和触发历史两个入口打开同一对运行、写同一句来历、摘要一致
- check-schedules:previous 为 false 时看得见的字(含键盘聚焦、390px 窄屏);两个入口拿同一个基准
This commit is contained in:
ThearchyHelios
2026-10-06 12:05:40 +08:00
parent 544d44bdc2
commit a18b24ab6f
5 changed files with 192 additions and 27 deletions
+15 -6
View File
@@ -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: '正在对比两次运行…',
+3 -2
View File
@@ -17,8 +17,9 @@ export const comparePath = (target: string, base?: string | null) =>
const NOTE_DELAY_MS = 300
/**
* 运行详情「更多」菜单里的「与上一次对比」。先问服务端默认的基准(同一定时任务的上一次,没有就同一工作流、同一类别的
* 上一次),找到了再跳到对比页;找不到时留在原页,提示服务端给的原因,并给「手动选择基准」去对比页自己挑。
* 运行详情「更多」菜单里的「与上一次对比」。先问服务端默认的基准(同一定时任务上一次成功的运行,没有就同一工作流、
* 同一类别上一次成功的运行;定时任务触发历史里同名的入口由服务端用同一个函数挑,两处比的是同一对运行),找到了再跳到
* 对比页;找不到时留在原页,提示服务端给的原因,并给「手动选择基准」去对比页自己挑。
* 这几种不给点,点了也只会被拒:没封存、还在运行的(对比只认已封存的运行,409);不属于已保存工作流的——画布、问数据
* 里没保存就运行的,和工作流删掉之后 workflow_id 被置空的(服务端找不到同一工作流的运行,手动选也是 422)。
*
+12 -7
View File
@@ -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>
+85 -5
View File
@@ -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)
+77 -7
View File
@@ -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]')