| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- /**
- * T16 数据新鲜度与过期状态 纯逻辑检查(票 16)。
- *
- * 零依赖 node 断言脚本(仓库没配测试运行器,约定见 t01-panel-catalog.mjs)。
- * 运行:node .scratch/cockpit-shell-panel-layout/verify/t16-freshness.mjs
- *
- * 七段(每条对着票 16 的验收原句):
- * A 验收 1:显示最近一次成功更新时间(缺失显示占位符,**不是** 00:00:00)
- * B 验收 2:新鲜 / 过期 / 不可用 / 部分失败**四态可区分**,文案各不相同
- * C 验收 3:**连续两个**配置周期未成功更新 → 过期或不可用(连续,不是累计)
- * D 验收 4:过期数据**保留**最后已知值并带过期标识(不伪装成实时)
- * E 验收 5:缺失指标显示占位符,**不显示虚假零值**(0 是有效值,null 才是缺失)
- * F 验收 6:刷新周期(前端轮询 + WS 断线也算失败周期)真的按周期跑、能停干净
- * G 静态守卫:组件/store 不自己拼文案与色键;domain 纯函数;周期来自配置
- *
- * 口径来源(维护者 2026-09-24 定):前端轮询 + WS 断线也算失败周期;
- * 「过期/不可用」复用票 05 的分区状态词汇;四态演示用注入式时钟 + mock 开关。
- */
- import { readFileSync } from 'node:fs'
- import { createPinia, setActivePinia } from 'pinia'
- import {
- EXPIRED_AFTER_FAILURES,
- FRESHNESS_PLACEHOLDER,
- FRESHNESS_VIEW,
- clockText,
- emptyFreshness,
- freshnessOf,
- freshnessText,
- freshnessView,
- isRetained,
- markFailure,
- markSuccess,
- overallFreshness,
- overallFreshnessText,
- sectionStateOf,
- sectionStateOfFreshnessView,
- sinceSuccessText,
- toMillis,
- viewFromBackendFreshness,
- viewFromSectionState,
- } from '../../../src/modules/cockpit/domain/freshnessModel.js'
- import { EXPIRED_MISSED_CYCLES, FRESHNESS, dataAgeText, isExpired } from '../../../src/modules/cockpit/domain/freshness.js'
- import {
- DEFAULT_REFRESH_SECONDS,
- getFreshnessState,
- isPolling,
- resetFreshness,
- runCycle,
- startPolling,
- stopPolling,
- } from '../../../src/modules/cockpit/services/freshnessService.js'
- import { useRuntimeStore } from '../../../src/modules/cockpit/stores/runtimeStore.js'
- import { normalizeConfig } from '../../../src/modules/cockpit/services/cockpitConfig.js'
- const checks = []
- function check(name, pass, evidence) {
- checks.push({ name, pass })
- console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${evidence ? ` — ${evidence}` : ''}`)
- }
- const moduleRoot = new URL('../../../src/modules/cockpit/', import.meta.url)
- const read = (relative) => readFileSync(new URL(relative, moduleRoot), 'utf8')
- const code = (text) => text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
- const readCode = (relative) => code(read(relative))
- const codeVue = (text) => code(text.replace(/<!--[\s\S]*?-->/g, ''))
- const readVueCode = (relative) => codeVue(read(relative))
- setActivePinia(createPinia())
- /** 假定时器:把「两个周期」变成同步可控,不真的等 60 秒。 */
- function fakeTimers() {
- const tasks = []
- return {
- setTimeout: (fn, ms) => {
- const id = { fn, ms }
- tasks.push(id)
- return id
- },
- clearTimeout: (id) => {
- const index = tasks.indexOf(id)
- if (index >= 0) tasks.splice(index, 1)
- },
- async step() {
- const next = tasks.shift()
- if (!next) return false
- next.fn()
- await Promise.resolve()
- return true
- },
- pending: () => tasks.length,
- }
- }
- const T0 = Date.parse('2026-09-24T10:00:00+08:00')
- // ================================================================ A 最近成功更新时间
- {
- const fresh = markSuccess(emptyFreshness(), T0)
- const view = freshnessView(fresh, T0 + 5000)
- check(
- 'A1-shows-last-success-clock',
- /^\d{2}:\d{2}:\d{2}$/.test(view.lastSuccessClock) && view.sinceSuccess === '5 秒前',
- `最近一次成功更新 ${view.lastSuccessClock}(${view.sinceSuccess})`,
- )
- // 缺失必须是占位符,**不是** 00:00:00 —— 后者看起来像一个真实时刻。
- const none = freshnessView(emptyFreshness(), T0)
- check(
- 'A2-missing-time-is-placeholder',
- none.lastSuccessClock === FRESHNESS_PLACEHOLDER &&
- none.sinceSuccess === FRESHNESS_PLACEHOLDER &&
- clockText(null) === FRESHNESS_PLACEHOLDER &&
- clockText('not-a-time') === FRESHNESS_PLACEHOLDER &&
- clockText('') === FRESHNESS_PLACEHOLDER,
- `没有成功过时显示「${none.lastSuccessClock}」(不编一个 00:00:00)`,
- )
- check(
- 'A3-accepts-iso-and-epoch',
- clockText(T0) === clockText(new Date(T0).toISOString()) && toMillis('2026-09-24T10:00:00+08:00') === T0,
- '毫秒时间戳与 ISO 串(后端 updatedAt 的形状)都认,解析不出回 null',
- )
- check(
- 'A4-age-text-scales',
- sinceSuccessText(markSuccess(emptyFreshness(), T0), T0 + 90_000) === '2 分钟前' &&
- sinceSuccessText(markSuccess(emptyFreshness(), T0), T0 + 7200_000) === '2 小时前',
- '「多久之前」随量级换单位(秒/分钟/小时)',
- )
- }
- // ================================================================ B 四态可区分
- {
- const never = emptyFreshness()
- const current = markSuccess(never, T0)
- let stale = current
- stale = markFailure(stale, T0 + 1000, '取数失败')
- stale = markFailure(stale, T0 + 2000, '取数失败')
- let unavailable = never
- unavailable = markFailure(unavailable, T0 + 1000, '取数失败')
- const views = [freshnessOf(current), freshnessOf(stale), freshnessOf(unavailable), freshnessOf(never)]
- check(
- 'B1-four-states-distinct',
- freshnessOf(current) === FRESHNESS_VIEW.CURRENT &&
- freshnessOf(stale) === FRESHNESS_VIEW.STALE &&
- freshnessOf(unavailable) === FRESHNESS_VIEW.UNAVAILABLE &&
- freshnessOf(never) === FRESHNESS_VIEW.EMPTY,
- `四态:${views.join(' / ')}(新鲜 / 过期 / 不可用 / 尚未取数)`,
- )
- const texts = views.map((view) => freshnessText(view))
- check(
- 'B2-four-texts-distinct',
- new Set(texts).size === 4 && freshnessText(FRESHNESS_VIEW.STALE).includes('过期') && !freshnessText(FRESHNESS_VIEW.STALE).includes('不可用'),
- `四句文案互不相同,且过期与不可用**说得不一样**:${texts.join(' | ')}`,
- )
- check(
- 'B3-partial-is-its-own-state',
- overallFreshness([FRESHNESS_VIEW.CURRENT, FRESHNESS_VIEW.UNAVAILABLE]) === 'partial' &&
- overallFreshness([FRESHNESS_VIEW.UNAVAILABLE, FRESHNESS_VIEW.UNAVAILABLE]) === FRESHNESS_VIEW.UNAVAILABLE &&
- /部分/.test(overallFreshnessText('partial')),
- `部分失败是独立状态:「${overallFreshnessText('partial')}」(一对一全坏才算整体不可用)`,
- )
- check(
- 'B4-maps-to-section-vocabulary',
- sectionStateOfFreshnessView(FRESHNESS_VIEW.STALE) === 'stale' &&
- sectionStateOfFreshnessView(FRESHNESS_VIEW.UNAVAILABLE) === 'unavailable' &&
- sectionStateOfFreshnessView(FRESHNESS_VIEW.CURRENT) === 'success',
- '四态映射到票 05 的分区状态词汇(不新造第二套状态词)',
- )
- check(
- 'B5-bridges-backend-conclusion',
- viewFromBackendFreshness(FRESHNESS.EXPIRED) === FRESHNESS_VIEW.STALE &&
- viewFromBackendFreshness(FRESHNESS.EMPTY) === FRESHNESS_VIEW.EMPTY &&
- viewFromBackendFreshness('WHATEVER') === '',
- '后端给了结论时能桥接过来(认不出的回空串,不猜)',
- )
- }
- // ================================================================ C 连续两个周期
- {
- check('C1-two-cycles-is-the-bar', EXPIRED_AFTER_FAILURES === 2 && EXPIRED_MISSED_CYCLES === 2, `判定阈值 = ${EXPIRED_AFTER_FAILURES}(与票 05 的 EXPIRED_MISSED_CYCLES 一致)`)
- // 一个周期失败:还不算过期(这正是"连续两个"的意思)。
- let state = markSuccess(emptyFreshness(), T0)
- state = markFailure(state, T0 + 1000)
- check(
- 'C2-one-failure-not-yet-stale',
- freshnessOf(state) === FRESHNESS_VIEW.CURRENT && state.consecutiveFailures === 1,
- `第 1 个周期失败:连续失败 ${state.consecutiveFailures},仍是「${freshnessText(freshnessOf(state))}」`,
- )
- // 第二个周期失败:过期。
- state = markFailure(state, T0 + 2000)
- check(
- 'C3-two-consecutive-failures-stale',
- freshnessOf(state) === FRESHNESS_VIEW.STALE,
- `第 2 个周期失败:连续失败 ${state.consecutiveFailures} → 「${freshnessText(freshnessOf(state))}」`,
- )
- // **连续性**是关键:中间成功一次就清零,不能累计。
- let resumed = markFailure(markSuccess(emptyFreshness(), T0), T0 + 1000)
- resumed = markSuccess(resumed, T0 + 2000)
- resumed = markFailure(resumed, T0 + 3000)
- check(
- 'C4-success-resets-streak',
- freshnessOf(resumed) === FRESHNESS_VIEW.CURRENT && resumed.consecutiveFailures === 1,
- `失败→成功→失败:连续失败回到 ${resumed.consecutiveFailures}(不是累计 2),仍是「${freshnessText(freshnessOf(resumed))}」`,
- )
- // 从未成功过 + 两个周期失败 → 不可用(没有旧值可留,与过期分开)。
- let never = emptyFreshness()
- never = markFailure(never, T0 + 1000)
- never = markFailure(never, T0 + 2000)
- check(
- 'C5-never-succeeded-is-unavailable',
- freshnessOf(never) === FRESHNESS_VIEW.UNAVAILABLE,
- `从未取到值又连续失败 ${never.consecutiveFailures} 次 → 「${freshnessText(freshnessOf(never))}」(与"过期"分开:没有旧值可留)`,
- )
- }
- // ================================================================ D 过期保留旧值
- {
- const succeeded = markSuccess(emptyFreshness(), T0)
- let stale = markFailure(markFailure(succeeded, T0 + 1000), T0 + 2000)
- check(
- 'D1-stale-keeps-last-success',
- stale.lastSuccessAt === T0 && isRetained(stale) === true,
- `过期后 lastSuccessAt 仍是第一次成功那一刻(${clockText(stale.lastSuccessAt)})—— 保留最后已知值,界面上带过期标识`,
- )
- check(
- 'D2-failure-does-not-erase-success-time',
- markFailure(succeeded, T0 + 1000).lastSuccessAt === T0 && stale.successes === 1,
- '失败**不覆盖**"最后一次成功是什么时候"(否则值班人员就查不到旧值有多旧)',
- )
- check(
- 'D3-retained-only-for-stale',
- isRetained(succeeded) === false && isRetained(emptyFreshness()) === false,
- '只有「过期」带保留标识;新鲜与尚未取数都不带',
- )
- check(
- 'D4-view-carries-retained-flag',
- freshnessView(stale, T0 + 3000).retained === true && freshnessView(stale, T0 + 3000).failures === 2,
- '视图对象把 retained / failures 一并交给界面(组件不自己算)',
- )
- }
- // ================================================================ E 不显示虚假零值
- {
- const missing = { dataAgeSeconds: null, updatedAt: null }
- check(
- 'E1-age-missing-is-placeholder',
- dataAgeText(missing) === '—' && dataAgeText({ dataAgeSeconds: undefined }) === '—',
- `年龄缺失显示「${dataAgeText(missing)}」,**不是**「0 秒前」(那会被读成刚更新过)`,
- )
- check(
- 'E2-real-zero-is-valid',
- dataAgeText({ dataAgeSeconds: 0 }) === '0 秒前' && clockText(T0) !== FRESHNESS_PLACEHOLDER,
- `后端真给 0 是有效数据:${dataAgeText({ dataAgeSeconds: 0 })}(照实显示,不当缺失)`,
- )
- check(
- 'E3-missed-cycles-drives-expiry',
- isExpired({ freshness: '', missedCycles: 2 }) === true &&
- isExpired({ freshness: '', missedCycles: 1 }) === false &&
- isExpired({ freshness: FRESHNESS.CURRENT, missedCycles: 5 }) === false,
- '后端只给 missedCycles 时按 >= 2 兜底;给了 freshness 结论则以它为准(前端不推翻后端)',
- )
- check(
- 'E4-no-zero-fabrication-on-empty',
- freshnessView(emptyFreshness(), T0).lastSuccessAt === null,
- '从未成功时不编一个时间戳出来(字段保持 null,界面显示占位符)',
- )
- // 变异测试暴露的缺口:`sinceSuccessText` 的"缺失回占位符"此前没有断言,
- // 把占位符改成「0 秒前」时闸门仍全绿(假的通过)。这里补上,两个入口都要守。
- check(
- 'E5-since-success-missing-is-placeholder',
- sinceSuccessText(emptyFreshness(), T0) === FRESHNESS_PLACEHOLDER &&
- sinceSuccessText({ lastSuccessAt: null }, T0) === FRESHNESS_PLACEHOLDER &&
- sinceSuccessText({ lastSuccessAt: 'garbage' }, T0) === FRESHNESS_PLACEHOLDER &&
- sinceSuccessText({ lastSuccessAt: T0 }, NaN) === FRESHNESS_PLACEHOLDER &&
- // 真给 0(刚刚更新)照实说 0 秒前,不混淆成缺失。
- sinceSuccessText({ lastSuccessAt: T0 }, T0) === '0 秒前',
- `两个年龄入口在缺失时都给「${FRESHNESS_PLACEHOLDER}」(不编「0 秒前」),而真的 0 秒照实显示`,
- )
- }
- // ================================================================ F 刷新周期(轮询)
- {
- // 周期真的按配置跑;注入时钟让"两个周期"同步可测。
- resetFreshness()
- const timers = fakeTimers()
- let clock = T0
- let pushLive = true
- let ticks = 0
- const states = []
- startPolling({
- refreshSeconds: 10,
- tick: () => {
- ticks += 1
- return { ok: true }
- },
- isPushLive: () => pushLive,
- now: () => clock,
- setTimeout: timers.setTimeout,
- clearTimeout: timers.clearTimeout,
- onState: (state) => states.push(freshnessOf(state)),
- })
- check(
- 'F1-first-cycle-runs-immediately',
- ticks === 1 && isPolling() && timers.pending() === 1,
- '启动即跑一轮(否则首屏要空等一个周期),并排好下一次',
- )
- check('F2-not-polling-before-start', DEFAULT_REFRESH_SECONDS === 30, `默认周期 ${DEFAULT_REFRESH_SECONDS} 秒(配置 runtime.refreshSeconds 可覆盖)`)
- // 推进两个周期(每周期取数成功)→ 一直新鲜。
- clock += 10_000
- await timers.step()
- clock += 10_000
- await timers.step()
- check(
- 'F3-successful-cycles-stay-current',
- ticks === 3 && freshnessOf(getFreshnessState()) === FRESHNESS_VIEW.CURRENT,
- `${ticks} 个周期都成功 → 仍是「${freshnessText(freshnessOf(getFreshnessState()))}」`,
- )
- // WS 断线:**也算这一周期失败**(维护者口径),取数成功也不改变结论。
- pushLive = false
- clock += 10_000
- await timers.step()
- const afterOne = getFreshnessState()
- clock += 10_000
- await timers.step()
- const afterTwo = getFreshnessState()
- check(
- 'F4-push-down-counts-as-failure',
- afterOne.consecutiveFailures === 1 &&
- freshnessOf(afterOne) === FRESHNESS_VIEW.CURRENT &&
- freshnessOf(afterTwo) === FRESHNESS_VIEW.STALE &&
- /状态推送未连接/.test(afterTwo.lastFailureReason),
- `WS 断线两个周期(REST 仍取数成功)→ 第 1 个周期仍是新鲜、第 2 个转「${freshnessText(freshnessOf(afterTwo))}」,原因是「${afterTwo.lastFailureReason}」`,
- )
- // 恢复推送 + 取数成功 → 回到新鲜(失败清零)。
- pushLive = true
- clock += 10_000
- await timers.step()
- check(
- 'F5-recovery-clears-failures',
- freshnessOf(getFreshnessState()) === FRESHNESS_VIEW.CURRENT && getFreshnessState().consecutiveFailures === 0,
- `链路恢复后回到「${freshnessText(freshnessOf(getFreshnessState()))}」(连续失败清零)`,
- )
- check(
- 'F6-tick-failure-counts',
- await (async () => {
- resetFreshness()
- const t2 = fakeTimers()
- let now2 = T0
- startPolling({
- refreshSeconds: 5,
- tick: () => false,
- isPushLive: () => true,
- now: () => now2,
- setTimeout: t2.setTimeout,
- clearTimeout: t2.clearTimeout,
- })
- // 启动即跑一轮(失败),再推进一个周期 → 连续 2 次失败。
- now2 += 5000
- await t2.step()
- const state = getFreshnessState()
- stopPolling()
- return state.consecutiveFailures === 2
- })(),
- '`tick` 返回 false 也算失败周期(取数失败与 WS 断线两条路都进同一本账)',
- )
- // 停止必须清干净(票 15 刚在"漏取消定时器"上栽过)。
- stopPolling()
- check(
- 'F7-stop-clears-timer',
- isPolling() === false && timers.pending() === 0,
- `stopPolling 后定时器剩 ${timers.pending()} 个、isPolling=${isPolling()}`,
- )
- check(
- 'F8-tick-throw-is-failure',
- (() => {
- resetFreshness()
- const t3 = fakeTimers()
- startPolling({
- refreshSeconds: 5,
- tick: () => {
- throw new Error('boom')
- },
- isPushLive: () => true,
- now: () => T0,
- setTimeout: t3.setTimeout,
- clearTimeout: t3.clearTimeout,
- })
- const state = getFreshnessState()
- stopPolling()
- return state.consecutiveFailures === 1 && /boom/.test(state.lastFailureReason)
- })(),
- '`tick` 抛错也收成失败周期(不把异常冒成页面级报错)',
- )
- resetFreshness()
- }
- // ================================================================ F9 store 接线(推送到达 = 链路活着)
- {
- const runtimeStore = useRuntimeStore()
- runtimeStore.reset()
- runtimeStore.startFreshness({
- refreshSeconds: 10,
- tick: () => ({ ok: true }),
- now: () => T0,
- })
- runtimeStore.stopFreshness()
- // 没有连接时 WS 不可用 → 启动那一轮**算失败**(这正是维护者口径:断线也算失败周期)。
- const beforeFailures = runtimeStore.freshnessState.consecutiveFailures
- const beforeSuccesses = runtimeStore.freshnessState.successes
- // 推送到达 → 连续失败清零并记一次成功(否则安静的推送周期会被误判成过期)。
- runtimeStore.ingest({ deviceCode: 'EQ-1', monitoringPointCode: 'MP-1', status: 'ALARM', serverUpdatedAt: '2026-09-24T10:00:05+08:00' })
- check(
- 'F9-push-arrival-counts-as-live',
- beforeFailures >= 1 &&
- runtimeStore.freshnessState.consecutiveFailures === 0 &&
- runtimeStore.freshnessState.successes > beforeSuccesses,
- `WS 未连时启动那轮先记失败(${beforeFailures} 次);推送到达后连续失败清零、成功数 ${beforeSuccesses} → ${runtimeStore.freshnessState.successes}`,
- )
- check(
- 'F10-store-exposes-view-for-ui',
- typeof runtimeStore.freshness.view === 'string' &&
- typeof runtimeStore.freshness.text === 'string' &&
- typeof runtimeStore.retained === 'boolean',
- `store 给出视图对象(view=${runtimeStore.freshness.view}、text=「${runtimeStore.freshness.text}」)供组件直接消费`,
- )
- runtimeStore.reset()
- }
- // ================================================================ G 静态守卫
- {
- const shellCode = readVueCode('CockpitShell.vue')
- check(
- 'G1-shell-shows-freshness',
- /data-freshness/.test(shellCode) && /runtimeStore\.freshness/.test(shellCode),
- '外壳把新鲜度画出来(data-freshness / data-freshness-at 是走查断言面)',
- )
- check(
- 'G2-shell-does-not-own-wording',
- !/数据已过期|数据不可用(尚未/.test(shellCode),
- '外壳不自己拼状态文案(文案只在 domain/freshnessModel.js 一份)',
- )
- check(
- 'G3-shell-stops-polling-on-unmount',
- /stopFreshness\(\)/.test(readVueCode('CockpitShell.vue')),
- '离开外壳必须停轮询(定时器不跟着页面跑下去)',
- )
- check(
- 'G4-domain-pure-and-no-timer',
- !/\bfetch\s*\(|XMLHttpRequest|setTimeout|setInterval/.test(read('domain/freshnessModel.js')) &&
- !/from '\.\.\/services\//.test(read('domain/freshnessModel.js')),
- 'domain/freshnessModel.js 是纯函数(不发请求、不 import service、不自己起定时器)',
- )
- check(
- 'G5-service-timers-are-injectable',
- /config\.setTimeout \|\|/.test(readCode('services/freshnessService.js')) &&
- /config\.clearTimeout \|\|/.test(readCode('services/freshnessService.js')),
- '轮询服务的定时器可注入(单测与走查用假定时器演示两个周期)',
- )
- check(
- 'G6-period-comes-from-config',
- (() => {
- const { config, issues } = normalizeConfig({ runtime: { refreshSeconds: 15 } })
- const bad = normalizeConfig({ runtime: { refreshSeconds: 0 } })
- return config.runtime.refreshSeconds === 15 && bad.config.runtime.refreshSeconds === 30 && bad.issues.length > 0
- })(),
- '周期来自配置 runtime.refreshSeconds;0/负数被拒并退回默认(否则"两个周期"永远不成立)',
- )
- check(
- 'G7-no-credential-in-freshness-modules',
- !/Authorization|Bearer|api\.token|ws\.token/.test(read('domain/freshnessModel.js')) &&
- !/Authorization|Bearer/.test(readCode('services/freshnessService.js')),
- '新鲜度两层都不碰凭据',
- )
- check(
- 'G8-store-owns-the-accounting',
- /recordExternal/.test(readCode('stores/runtimeStore.js')) && /isPushLive: \(\) => live\.value/.test(readCode('stores/runtimeStore.js')),
- 'WS 可用性取自 store 的连接状态(方向不反:service 不认识 store)',
- )
- // 「部分失败」必须真的被界面消费 —— 变异测试暴露过:纯函数写了却没人用(死代码),
- // 验收第 2 条会因此名存实亡。
- check(
- 'G9-partial-state-is-consumed-by-ui',
- /overallFreshness\(/.test(shellCode) &&
- /overallFreshnessText\(/.test(shellCode) &&
- /data-freshness-partial/.test(shellCode),
- '「部分失败」被外壳真的画出来(data-freshness-partial),不是只写在 domain 里',
- )
- check(
- 'G10-bridges-section-to-freshness',
- viewFromSectionState('stale') === FRESHNESS_VIEW.STALE &&
- viewFromSectionState('failed') === FRESHNESS_VIEW.UNAVAILABLE &&
- viewFromSectionState('') === '' &&
- sectionStateOf({ state: 'success' }) === 'success' &&
- sectionStateOf(null) === '',
- '分区状态与新鲜度四态之间有唯一一处显式翻译(界面不自己数数)',
- )
- }
- const failed = checks.filter((item) => !item.pass)
- console.log('')
- console.log(`T16 数据新鲜度检查:${checks.length - failed.length}/${checks.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:')
- for (const item of failed) console.log(` · ${item.name}`)
- process.exit(1)
- }
- console.log('T16 数据新鲜度全部通过。')
|