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}
-
{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()
}
})