diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 8ea7b9fb..76b9a95e 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -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, pickBase: '更换基准', pickHint: '同一工作流最近的运行,未封存的不能作为基准', @@ -3195,7 +3204,7 @@ export const RUN_COMPARE_TEXT = { pickGone: (id: string, missing: boolean) => `${id}(${missing ? '记录不存在' : '无法读取'})`, // 页面状态 noTarget: '未指定要对比的运行', - noTargetHint: '在运行详情的「更多」菜单中选择「与上一次对比」,即可对照本次与上一次运行', + noTargetHint: '在运行详情的「更多」菜单中选择「与上一次对比」,即可对照本次与上一次成功的运行', toRuns: '前往记录', finding: '正在查找基准运行…', loading: '正在对比两次运行…', diff --git a/frontend/src/pages/runs/compare/entry.tsx b/frontend/src/pages/runs/compare/entry.tsx index cee05d99..f0e55a3e 100644 --- a/frontend/src/pages/runs/compare/entry.tsx +++ b/frontend/src/pages/runs/compare/entry.tsx @@ -17,8 +17,9 @@ export const comparePath = (target: string, base?: string | null) => const NOTE_DELAY_MS = 300 /** - * 运行详情「更多」菜单里的「与上一次对比」。先问服务端默认的基准(同一定时任务的上一次,没有就同一工作流、同一类别的 - * 上一次),找到了再跳到对比页;找不到时留在原页,提示服务端给的原因,并给「手动选择基准」去对比页自己挑。 + * 运行详情「更多」菜单里的「与上一次对比」。先问服务端默认的基准(同一定时任务上一次成功的运行,没有就同一工作流、 + * 同一类别上一次成功的运行;定时任务触发历史里同名的入口由服务端用同一个函数挑,两处比的是同一对运行),找到了再跳到 + * 对比页;找不到时留在原页,提示服务端给的原因,并给「手动选择基准」去对比页自己挑。 * 这几种不给点,点了也只会被拒:没封存、还在运行的(对比只认已封存的运行,409);不属于已保存工作流的——画布、问数据 * 里没保存就运行的,和工作流删掉之后 workflow_id 被置空的(服务端找不到同一工作流的运行,手动选也是 422)。 * diff --git a/frontend/src/pages/schedules/FireHistory.tsx b/frontend/src/pages/schedules/FireHistory.tsx index aeafd8de..1c8546d7 100644 --- a/frontend/src/pages/schedules/FireHistory.tsx +++ b/frontend/src/pages/schedules/FireHistory.tsx @@ -14,7 +14,8 @@ const PAGE = 20 /** * 触发历史:每次触发一行,新的在前,20 条一页、「加载更多」往后接。计划时间按定时任务的时区写;跳过、发起失败 * 写原因;发起了的链到那次运行、写运行现在的状态;运行成功结束后服务端和同一任务上一次成功的运行比过的,另给 - * 「与上一次对比」;发出去的通知逐条写结果。refreshKey 变了(立即运行之后)从头重取 + * 「与上一次对比」(基准不是紧挨着的上一次时写「与上一次成功的运行对比」);发出去的通知逐条写结果。refreshKey 变了 + * (立即运行之后)从头重取 */ export function FireHistory({ schedule, refreshKey }: { schedule: Schedule; refreshKey: number }) { const [items, setItems] = useState(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 ( - {T.compareLink} + {previous ? T.compareLink : T.compareLinkLastOk} {count && ( <> · diff --git a/scripts/check-compare.mjs b/scripts/check-compare.mjs index f980e46d..c00a9177 100644 --- a/scripts/check-compare.mjs +++ b/scripts/check-compare.mjs @@ -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) diff --git a/scripts/check-schedules.mjs b/scripts/check-schedules.mjs index f5c5b0bc..634fb9dc 100644 --- a/scripts/check-schedules.mjs +++ b/scripts/check-schedules.mjs @@ -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]')