diff --git a/frontend/src/run/EvidencePanel.tsx b/frontend/src/run/EvidencePanel.tsx index 608a8380..4d8ccc9c 100644 --- a/frontend/src/run/EvidencePanel.tsx +++ b/frontend/src/run/EvidencePanel.tsx @@ -323,7 +323,7 @@ function SegmentBody({ panelId, doc, artifact, runId, runClass, seg, unit, block // 每份快照里这个指标用得到的行:别的行勾了不影响结果,表格里不让勾 const whatifUsable = useMemo(() => usableRows(pickMetric(whatifView.data, whatifMetric)), [whatifView.data, whatifMetric]) // 能勾行的查询步骤:指标的输入(单元格取数、Agent 字段)落在它上面。合并查询的输入步骤不给——指标引用的是合并结果。 - // 整个指标推演不了(口径卡没记输入)时一行都不给勾 + // 整个指标推演不了(口径卡没记输入、用到了遮罩列、有输入还原不了)时一行都不给勾 const pickable = new Set(wholeReason(whatifView.data, whatifMetric) ? [] : inputs.map((inp) => inp.artifact).filter((a): a is string => !!a)) const queryLink = (artifactOf?: string) => { diff --git a/frontend/src/run/WhatIf.tsx b/frontend/src/run/WhatIf.tsx index b9596344..16e06f28 100644 --- a/frontend/src/run/WhatIf.tsx +++ b/frontend/src/run/WhatIf.tsx @@ -25,6 +25,8 @@ import type { EvidenceStep, WhatIfInput, WhatIfMetric, WhatIfOverrides, WhatIfRe * 整份快照的(SnapshotRows)。只有这个指标的输入实际用到的行可勾(row_span),别的行勾了也不影响结果 * * 不能改的输入(文本、对不上快照的列表、遮罩列、决定取哪一格的下标)照实显示,原因列在「不支持推演的输入」里。 + * 整个指标推演不了的(口径卡没记输入、用到了遮罩列、有输入还原不了或所在的快照读不出来,见 wholeReason)只写原因: + * 不写「无变化」,不画输入框、勾选框,也不列报告里引用它的地方——改了、勾了也推演不出来。 */ /** 输入框停顿多久提交一次:边打字边请求,结果跟着抖 */ @@ -197,7 +199,11 @@ export function pickMetric(data: WhatIfResult | undefined, metric: string | unde return data?.metrics.find((x) => String(x.id) === metric) ?? data?.metrics[0] } -/** 整个指标推演不了(口径卡没记输入):unsupported 里 input 为空的那一条 */ +/** + * 整个指标推演不了(口径卡没记输入、用到了遮罩列、有输入还原不了或所在的快照读不出来):unsupported 里 input 为空的 + * 那一条。不论改什么都推演不了,后端不改动也照实给;排除行后才说不准的(只在这次改动下推演不了)没有这一条,照常画 + * 输入和勾选框,好让人把行勾回去 + */ export const wholeReason = (data: WhatIfResult | undefined, metric: string | undefined) => data?.unsupported.find((u) => String(u.metric) === metric && u.input == null)?.reason @@ -307,7 +313,7 @@ export function WhatIf({ id, runId, report, metric, draft, view, queryOf, onLoca const failed = status === 'error' const m = pickMetric(data, metric) const unsupported = (data?.unsupported ?? []).filter((u) => String(u.metric) === metric) - // 整个指标推演不了(口径卡没记输入):只说原因,不画输入、不写「无变化」 + // 整个指标推演不了(口径卡没记输入、用到了遮罩列、有输入还原不了):只说原因,不画输入、不写「无变化」 const whole = wholeReason(data, metric) const unknown = !!m && m.rendered_new == null const changedSegs = (data?.segments ?? []).filter((s) => s.changed) @@ -344,12 +350,14 @@ export function WhatIf({ id, runId, report, metric, draft, view, queryOf, onLoca {whole ? ( <> {m.rendered_old} -

{whole}

+

{whole}

) : ( <> - {m.reason &&

{m.reason}

} + {m.reason && ( +

{m.reason}

+ )} {m.status === 'ok' && m.changed && m.substituted && (
{WHATIF_TEXT.substituted}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 3c450062..22cdf55e 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -3052,7 +3052,7 @@ export interface WhatIfInput { artifact?: string locator?: EvidenceLocator kind: 'number' | 'text' | 'bool' | 'missing' | 'container' | (string & {}) - /** 数值(或原本缺失)的标量、不是遮罩列、不决定取哪一格:可以改 */ + /** 数值(或原本缺失)的标量、不是遮罩列、不决定取哪一格、还原得出:可以改。整个指标推演不了时一律为 false */ editable: boolean /** 原值、推演用的值。容器写大概(「[12 项]」);遮罩列的一律是 null */ old?: unknown @@ -3062,7 +3062,7 @@ export interface WhatIfInput { excluded_rows?: number[] /** * 这个输入实际用到的快照行(含两端,从 0 数):单格一行、数组字段一段、整份查询结果全部。排除别的行影响不到它, - * 界面只让这几行可勾。遮罩的、快照读不出来的没有 + * 界面只让这几行可勾。遮罩的、还原不了的(完整值找不回来、快照读不出来)没有,整个指标推演不了时一律没有 */ row_span?: [number, number] masked?: boolean @@ -3072,7 +3072,8 @@ export interface WhatIfInput { /** * unchanged:没碰到改动,原值原样交回;ok:重算了;excluded:排除行后缺了输入,算不出来;error:新值让计算出错(除以零……); - * unsupported:有输入的完整值找不回来,推演不了;masked:用到了遮罩列,不推演 + * unsupported:推演不了(有输入还原不了、口径卡没记输入,或者排除行后说不准受不受影响);masked:用到了遮罩列, + * 不推演。整个指标推演不了的不改动也是 unsupported / masked(unsupported 里另有一条 input 为空的) */ export type WhatIfStatus = 'unchanged' | 'ok' | 'excluded' | 'error' | 'unsupported' | 'masked' @@ -3112,7 +3113,10 @@ export interface WhatIfSegment { export interface WhatIfUnsupported { metric: string - /** null:整个指标都推演不了(口径卡没记输入) */ + /** + * null:整个指标都推演不了(口径卡没记输入、用到了遮罩列、有输入还原不了或所在的快照读不出来),reason 和指标上的 + * 一样。不论改什么都推演不了,不改动也有这一条;界面据此只写原因,不画输入框、勾选框 + */ input: string | null reason: string } diff --git a/scripts/check-all.mjs b/scripts/check-all.mjs index 25d548b5..f8613be2 100644 --- a/scripts/check-all.mjs +++ b/scripts/check-all.mjs @@ -96,7 +96,7 @@ const ESTIMATE = { stream: 173, chat: 160, studio: 149, manage: 145, runs: 112, publish: 92, shell: 87, evidence: 91, versions: 60, catalog: 45, 'ui-kit': 70, ui: 61, guards: 8, 'canvas-layout': 2, copy: 2, tokens: 1, trace: 1, decode: 1, // 占位,换成真检查时按实测改 - schedules: 1, compare: 1, verify: 1, inquiry: 1, bundle: 1, whatif: 28, + schedules: 1, compare: 1, verify: 1, inquiry: 1, bundle: 1, whatif: 33, } // 最慢的两项按段拆成几份。用的是它们自己的段过滤:最后一份用 *_SKIP 兜住「其余全部」,以后新加的段 // 不用改这里也跑得到(拆出去的那几份要是一段都没对上,那一份会报「一项都没跑」)。 diff --git a/scripts/check-whatif.mjs b/scripts/check-whatif.mjs index 53cb6eba..5258e5d2 100644 --- a/scripts/check-whatif.mjs +++ b/scripts/check-whatif.mjs @@ -4,7 +4,8 @@ // 输入路径和行号不对(行号对人从 1 数,对接口从 0 数)、勾选框在不推演时也冒出来、恢复原值之后还留着一行没勾回去、 // 不支持的输入一声不吭、窄屏(390px)被推演区撑出横向滚动、浅色主题下横幅看不清。还有复核第 1 轮查出来的:整份 // 结果作输入时无行可勾、窗口里勾了不起作用的行也给勾、勾完行结果不在视野里、加载中和失败后还摆着上一次的答复、 -// 封存核对没过不说、整个指标推演不了时又写「无可推演」又写「无变化」。 +// 封存核对没过不说、整个指标推演不了时又写「无可推演」又写「无变化」。复核第 2 轮:用到了遮罩列、有输入还原不了的 +// 指标一打开仍写「无变化」,还给可改的输入框和可勾的行(改了、勾了才说推演不了)。 // // 跑在预览页上:/preview.html?syn=whatif(宽屏右侧弹出、窄屏底部抽出)和 ?syn=whatif-rail(360px 画布右栏)。 // 文档是 frontend/src/run/__tests__/evidence-query.json 里后端 compose_doc 真跑出来的(只用通用名);片段接口 @@ -42,15 +43,35 @@ async function section(name, fn) { // ---------------------------------------------------------------- 夹具 /** - * 片段接口的答复:夹具里的原样。s22 改成老运行的容器输入(推演不了的那种);s18 改成整份查询结果作输入 - * (len(vars.orders.rows)):证据链里没有它的查询步骤,和后端真跑出来的一样 + * 片段接口的答复:夹具里的原样,几处改成推演要用到的样子(推演接口的假答复和这里对得上): + * - s22:老运行的容器输入(没存完整值)乘一个本来能改的数,整个指标推演不了 + * - s10:Q1 的 gmv 列设了遮罩,整个指标推演不了 + * - s8:订单数按文本条件取(文本输入不能改),订单数本身能改 + * - s6:销售额是一个加工过的列表求和(对不上任何快照),没有能改的输入、也没有能勾的行 + * - s18:整份查询结果作输入(len(vars.orders.rows)):证据链里没有它的查询步骤,和后端真跑出来的一样 */ function segmentReply(sid) { const body = structuredClone(fx.segments[sid]) if (sid === 's22') { - body.chain[0] = { ...body.chain[0], expression: 'sum(vars.adjust.items)', substituted: 'sum(vars.adjust.items)' } + body.chain[0] = { ...body.chain[0], expression: 'sum(vars.adjust.items) * vars.adjust.k', + substituted: 'sum(vars.adjust.items) * 1' } body.chain[1] = { step: 'input', path: 'vars.adjust.items', value: '[4 项]', node_id: 'adjust', via: 'code', field: 'items', role: 'compute', status: 'ok' } + body.chain.push({ step: 'input', path: 'vars.adjust.k', value: 1, node_id: 'adjust', via: 'code', field: 'k', + role: 'compute', status: 'ok' }) + } + if (sid === 's10') body.chain = body.chain.map((s) => (s.step === 'query' ? { ...s, masked: ['gmv'] } : s)) + if (sid === 's8') { + body.chain[0] = { ...body.chain[0], expression: "vars.kpi.order_cnt if vars.kpi.region == '华东' else 0", + substituted: "1234 if '华东' == '华东' else 0" } + body.chain.splice(2, 0, { step: 'input', path: 'vars.kpi.region', value: '华东', node_id: 'parse', via: 'transform', + field: 'region', status: 'ok' }) + } + if (sid === 's6') { + body.chain = [ + { ...body.chain[0], expression: 'sum(vars.kpi.daily)', substituted: 'sum(vars.kpi.daily)' }, + { step: 'input', path: 'vars.kpi.daily', value: '[7 项]', node_id: 'daily', via: 'transform', field: 'daily', status: 'ok' }, + ] } if (sid === 's18') { body.chain = [ @@ -70,6 +91,11 @@ const Q3 = inputsOf('s70')[0].artifact const SNAP = fx.snapshots.Q3.content const thousands1 = (v) => v.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) const NO_LINEAGE = '这个指标生成于系统升级前,口径卡没有记录输入,不能推演' +const MASKED_METRIC = '这个指标用到了设置了遮罩的列,不能推演' +const LOST = '无法推演:vars.adjust.items(当时没有保存这个输入的完整值)' +/** 整个指标推演不了时后端给的样子:不改动也是 unsupported / masked,新值、变化一概为空 */ +const blocked = (status, reason) => ({ new: null, delta: null, rendered_new: null, rendered_delta: null, changed: null, + status, reason, substituted: null }) const AFFECTED = [{ node_id: 'yearly', artifact: 'b1'.repeat(32), caliber: '年报口径', version: 'v1', metrics: ['y'], direct: false }] /** 推演接口在这里按后端的规则算:给了新值的用新值;cell / agent 字段那一格所在的行被排除了,指标排除后无法计算 */ @@ -109,21 +135,58 @@ function whatifReply(body) { } if (body?.metric === 'refund_rate') { // 升级前的口径卡:没记输入,整个指标推演不了 - return { ...base, metrics: [{ id: 'refund_rate', name: '退款率', unit: '', old: 0.0235, new: 0.0235, delta: 0, - rendered_old: '2.35%', rendered_new: '2.35%', rendered_delta: null, changed: false, status: 'unchanged', reason: null, - substituted: null, inputs: [] }], + return { ...base, metrics: [{ id: 'refund_rate', name: '退款率', unit: '', old: 0.0235, rendered_old: '2.35%', + ...blocked('unsupported', NO_LINEAGE), inputs: [] }], segments: [{ report: 'write', unit: fx.segments.s16.unit.id, sid: 's16', metric: 'refund_rate', rendered_old: '2.35%', - rendered_new: '2.35%', changed: false }], + rendered_new: null, changed: null }], unsupported: [{ metric: 'refund_rate', input: null, reason: NO_LINEAGE }] } } if (body?.metric === 'adj') { - return { ...base, metrics: [{ id: 'adj', name: '调整后销售额', unit: '元', old: 44000, new: 44000, delta: 0, - rendered_old: '44,000.0元', rendered_new: '44,000.0元', rendered_delta: null, changed: false, status: 'unchanged', - reason: null, substituted: 'sum(vars.adjust.items)', + // 老运行:容器没存完整值。乘的那个数本来能改,指标整个推演不了,也就不让改 + return { ...base, metrics: [{ id: 'adj', name: '调整后销售额', unit: '元', old: 44000, rendered_old: '44,000.0元', + ...blocked('unsupported', LOST), inputs: [{ path: 'vars.adjust.items', via: 'code', node_id: 'adjust', kind: 'container', editable: false, - old: '[4 项]', new: '[4 项]', changed: false }] }], - segments: [{ report: 'write', unit: 'u4', sid: 's22', metric: 'adj', rendered_old: '44,000.0元', rendered_new: '44,000.0元', changed: false }], - unsupported: [{ metric: 'adj', input: 'vars.adjust.items', reason: '无法推演:当时没有保存这个输入的完整值' }] } + old: '[4 项]', new: null, changed: false }, + { path: 'vars.adjust.k', via: 'code', node_id: 'adjust', kind: 'number', editable: false, old: 1, new: 1, changed: false }] }], + segments: [{ report: 'write', unit: fx.segments.s22.unit.id, sid: 's22', metric: 'adj', rendered_old: '44,000.0元', + rendered_new: null, changed: null }], + unsupported: [{ metric: 'adj', input: null, reason: LOST }, + { metric: 'adj', input: 'vars.adjust.items', reason: '无法推演:当时没有保存这个输入的完整值' }] } + } + if (body?.metric === 'aov') { + // gmv 列设了遮罩:那一格不给值;订单数本来能改、它那一行本来能勾,指标整个推演不了,一样不让动 + const cell = (path, column) => ({ path, via: 'agent_field', node_id: 'fetch', artifact: Q1, locator: { row: 0, column } }) + return { ...base, metrics: [{ id: 'aov', name: '客单价', unit: '元', old: 37.0, rendered_old: '37.0元', + ...blocked('masked', MASKED_METRIC), + inputs: [{ ...cell('vars.kpi.gmv', 'gmv'), kind: 'number', editable: false, old: null, new: null, changed: false, masked: true }, + { ...cell('vars.kpi.order_cnt', 'order_cnt'), kind: 'number', editable: false, old: 1234, new: 1234, changed: false }] }], + segments: [{ report: 'write', unit: fx.segments.s10.unit.id, sid: 's10', metric: 'aov', rendered_old: '37.0元', + rendered_new: null, changed: null }], + unsupported: [{ metric: 'aov', input: null, reason: MASKED_METRIC }, + { metric: 'aov', input: 'vars.kpi.gmv', reason: '这一列在数据源中设置了遮罩,不能用于推演' }] } + } + if (body?.metric === 'orders') { + // 推演得了的指标里有一个文本输入:照实显示、列出原因,订单数照常能改 + return { ...base, metrics: [{ id: 'orders', name: '订单数', unit: '单', old: 1234, new: 1234, delta: 0, + rendered_old: '1,234单', rendered_new: '1,234单', rendered_delta: null, changed: false, status: 'unchanged', reason: null, + substituted: "1234 if '华东' == '华东' else 0", + inputs: [{ path: 'vars.kpi.order_cnt', via: 'agent_field', node_id: 'fetch', artifact: Q1, + locator: { row: 0, column: 'order_cnt' }, row_span: [0, 0], kind: 'number', editable: true, old: 1234, new: 1234, changed: false }, + { path: 'vars.kpi.region', via: 'transform', node_id: 'parse', kind: 'text', editable: false, old: '华东', new: '华东', changed: false }] }], + segments: [{ report: 'write', unit: fx.segments.s8.unit.id, sid: 's8', metric: 'orders', rendered_old: '1,234单', + rendered_new: '1,234单', changed: false }], + unsupported: [{ metric: 'orders', input: 'vars.kpi.region', reason: '不是数值,不能修改' }] } + } + if (body?.metric === 'gmv') { + // 加工过的列表(对不上任何快照):不能改、也没有行可勾,推演得了却没什么可动 + return { ...base, metrics: [{ id: 'gmv', name: '销售额', unit: '元', old: 45678.5, new: 45678.5, delta: 0, + rendered_old: '45,678.5元', rendered_new: '45,678.5元', rendered_delta: null, changed: false, status: 'unchanged', + reason: null, substituted: 'sum(vars.kpi.daily)', + inputs: [{ path: 'vars.kpi.daily', via: 'transform', node_id: 'daily', kind: 'container', editable: false, + old: '[7 项]', new: '[7 项]', changed: false }] }], + segments: [{ report: 'write', unit: fx.segments.s6.unit.id, sid: 's6', metric: 'gmv', rendered_old: '45,678.5元', + rendered_new: '45,678.5元', changed: false }], + unsupported: [{ metric: 'gmv', input: 'vars.kpi.daily', reason: '列表或对象不能直接修改,也没有对应的查询结果可以排除行' }] } } if (body?.metric === 'fx') { // 空态:原式里没有引用任何输入(写死的常数) @@ -436,22 +499,69 @@ await section('勾行后结果就在手边', async () => { await section('不支持的输入', async () => { const { ctx, page } = await open('whatif') - await openMetric(page, 's22') + await openMetric(page, 's8') await openWhatIf(page) - check('不支持推演的输入列出原因', (await text(page, '[data-whatif-unsupported-input="vars.adjust.items"]')) - .includes('无法推演:当时没有保存这个输入的完整值')) - check('容器输入不给输入框', await page.locator('[data-whatif-field]').count() === 0 - && (await text(page, '[data-whatif-input="vars.adjust.items"] [data-whatif-value]')).includes('[4 项]')) - check('没有能改的输入、也没有能勾的行:说一句为什么', (await text(page, '[data-whatif-nothing]')).includes('没有可以修改的输入')) + check('不支持推演的输入列出原因', (await text(page, '[data-whatif-unsupported-input="vars.kpi.region"]')) + .includes('不是数值,不能修改')) + check('文本输入照实显示、不给输入框;数值输入照常能改', await page.locator('[data-whatif-field]').count() === 1 + && (await text(page, '[data-whatif-input="vars.kpi.region"] [data-whatif-value]')).includes('华东') + && await page.locator('[data-whatif-input="vars.kpi.order_cnt"] [data-whatif-field]').count() === 1) + check('指标照常推演:原值 → 原值,标「无变化」', (await text(page, '[data-whatif-new]')) === '1,234单' + && (await text(page, '[data-whatif-delta]')) === '无变化') + check('能改的那一格所在的查询步骤里出现勾选框', await page.locator(`[data-ev-query="${Q1}"] [data-whatif-row="0"] input:not(:disabled)`).count() === 1) + await ctx.close() + + const bare = await open('whatif') + await openMetric(bare.page, 's6') + await openWhatIf(bare.page) + check('加工过的列表:列出原因、不给输入框', (await text(bare.page, '[data-whatif-unsupported-input="vars.kpi.daily"]')) + .includes('列表或对象不能直接修改') && await bare.page.locator('[data-whatif-field]').count() === 0 + && (await text(bare.page, '[data-whatif-input="vars.kpi.daily"] [data-whatif-value]')).includes('[7 项]')) + check('没有能改的输入、也没有能勾的行:说一句为什么', (await text(bare.page, '[data-whatif-nothing]')).includes('没有可以修改的输入')) + await bare.ctx.close() +}) + +/** 整个指标推演不了:展开推演,等结果区写出原因。交回结果区的字(压成一行,好写进检查项的说明) */ +async function openWhole(page) { + await page.click('[data-whatif-toggle]') + await page.waitForSelector('[data-whatif-result="whole"]') + return (await text(page, '[data-whatif-result]')).replace(/\s+/g, ' ').trim() +} + +await section('整个指标推演不了:输入还原不了', async () => { + const { ctx, page, sent, errors } = await open('whatif') + await openMetric(page, 's22') + const result = await openWhole(page) + check('一打开就写明哪个输入还原不了、为什么', result.includes(LOST), result) + check('不写「无变化」,也不画「→」', !result.includes('无变化') && !result.includes('→'), result) + check('本来能改的数也不给输入框,不列输入、不支持的输入、报告中会变的地方', + await page.locator('[data-whatif-field], [data-whatif-inputs], [data-whatif-unsupported], [data-whatif-segments]').count() === 0) + check('「恢复原值」不可点,只发过一次不带改动的请求', await page.locator('[data-whatif-reset]').isDisabled() && sent.length === 1 + && JSON.stringify(sent[0].body.overrides) === JSON.stringify({ inputs: {}, exclude_rows: {} })) + check('横幅照旧在', (await text(page, '[data-whatif-banner]')).trim() === BANNER) + await shot(page, 'whole-lost-dark') + check('页面没有报错', errors.length === 0, errors.join(';')) await ctx.close() }) -await section('整个指标推演不了', async () => { +await section('整个指标推演不了:用到了遮罩列', async () => { + const { ctx, page } = await open('whatif') + await openMetric(page, 's10') + const result = await openWhole(page) + check('一打开就写明用到了遮罩列', result.includes(MASKED_METRIC), result) + check('不写「无变化」,也不画「→」', !result.includes('无变化') && !result.includes('→'), result) + check('本来能改的订单数也不给输入框', await page.locator('[data-whatif-field], [data-whatif-inputs]').count() === 0) + check('查询步骤里不出勾选框,结果窗口照常显示(遮罩列写「已遮罩」)', await page.locator('[data-whatif-rows], [data-whatif-row]').count() === 0 + && await page.locator(`[data-ev-query="${Q1}"] table`).count() === 1 + && (await text(page, `[data-ev-query="${Q1}"] table`)).includes('已遮罩')) + check('不说报告里引用它的地方「不受影响」', await page.locator('[data-whatif-segments], [data-whatif-segments-none]').count() === 0) + await ctx.close() +}) + +await section('整个指标推演不了:口径卡没记输入', async () => { const { ctx, page } = await open('whatif') await openMetric(page, 's16') - await page.click('[data-whatif-toggle]') - await page.waitForSelector('[data-whatif-result="whole"]') - const result = await text(page, '[data-whatif-result]') + const result = await openWhole(page) check('只说原因:口径卡没有记录输入', result.includes('口径卡没有记录输入,不能推演'), result) check('不写「无变化」,也不说「没有引用任何输入」', !result.includes('无变化') && !result.includes('→') && await page.locator('[data-whatif-empty], [data-whatif-inputs], [data-whatif-unsupported]').count() === 0) @@ -572,6 +682,13 @@ await section('窄屏', async () => { await page.waitForSelector('[data-whatif-snapshot] [data-whatif-row]') const t = await overflow(page) check(`390px(${theme}):推演区里的勾行表格不横向溢出`, t.page <= 0 && t.body <= 1 && t.whatif <= 1, JSON.stringify(t)) + // 整个指标推演不了:结果区只有原值和原因,原因里的输入路径长也要折行 + await page.locator('[data-evidence-panel] button[aria-label="关闭证据"]').click() + await openMetric(page, 's22') + await openWhole(page) + const w = await overflow(page) + check(`390px(${theme}):整个指标推演不了时不横向溢出`, w.page <= 0 && w.body <= 1 && w.whatif <= 1, JSON.stringify(w)) + await shot(page, `narrow-whole-${theme}`) check(`390px(${theme}):页面没有报错`, errors.length === 0, errors.join(';')) await ctx.close() } @@ -626,6 +743,20 @@ await section('深浅色可读', async () => { check(`${theme}:横幅加粗`, c.weight >= 500, String(c.weight)) await shot(page, `open-${theme}`) await ctx.close() + + // 整个指标推演不了时,结果区里只剩一句原因:它就是要人读的那句 + const whole = await open('whatif', { theme }) + await openMetric(whole.page, 's10') + await openWhole(whole.page) + const r = await whole.page.evaluate(() => { + const reason = document.querySelector('[data-whatif-reason]') + const panel = document.querySelector('[data-evidence-panel]') + return { fg: getComputedStyle(reason).color, base: getComputedStyle(panel).backgroundColor } + }) + const reasonRatio = contrast(r.fg, r.base, r.base) + check(`${theme}:整个指标推演不了的原因对比度 ≥ 4.5`, reasonRatio >= 4.5, `${reasonRatio.toFixed(2)}(${r.fg} on ${r.base})`) + await shot(whole.page, `whole-masked-${theme}`) + await whole.ctx.close() } })