195 lines
8.1 KiB
JavaScript
195 lines
8.1 KiB
JavaScript
|
|
/**
|
|||
|
|
* RSSI 信号强度分档(协议定点:`rssi` 是 **0–99 的信号等级**,非 dBm)
|
|||
|
|
*
|
|||
|
|
* 覆盖**两端**(PC `software/web/src/utils/deviceStatus.ts` 与
|
|||
|
|
* 小程序 `software/h5-uniapp/src/utils/deviceStatus.ts`)的 `rssiSignal` / `rssiBarColors`,
|
|||
|
|
* 并断言**两端口径完全一致**(同一输入 → 同一 `level` / `bars` / `color`)。
|
|||
|
|
*
|
|||
|
|
* 为什么需要它:2026-09 之前两端**本来就不一致**——
|
|||
|
|
* - PC 是 5 档(≥70 绿 / ≥55 蓝 / ≥40 黄 / ≥30 橙 / <30 灰),格数随档位增删;
|
|||
|
|
* - 小程序只有 4 档(**没有 ≥70**)、30–39 用**红色**、且**恒渲染 5 格只改颜色**。
|
|||
|
|
* 本次收敛为统一 5 档后,用本用例把"两端一致"钉死:**只改一端就会红**。
|
|||
|
|
*
|
|||
|
|
* 另外断言**档位边界**与**颜色表**(防止有人"统一"成另一套阈值却没人发现)。
|
|||
|
|
*
|
|||
|
|
* 运行:node test/rssi_signal.test.mjs
|
|||
|
|
*/
|
|||
|
|
import assert from 'node:assert/strict'
|
|||
|
|
|
|||
|
|
import * as web from '../software/web/src/utils/deviceStatus.ts'
|
|||
|
|
import * as mp from '../software/h5-uniapp/src/utils/deviceStatus.ts'
|
|||
|
|
|
|||
|
|
/** 极简用例收集器(保持仓库零依赖) */
|
|||
|
|
const cases = []
|
|||
|
|
function test(name, fn) {
|
|||
|
|
cases.push([name, fn])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// 权威分档表(写死在这里 = 契约;与两个实现文件里的 RSSI_TIERS 必须一致)
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
const EXPECTED_TIERS = [
|
|||
|
|
{ min: 70, level: 4, bars: 5, color: '#16a34a', key: 'strong' },
|
|||
|
|
{ min: 55, level: 3, bars: 4, color: '#2563eb', key: 'good' },
|
|||
|
|
{ min: 40, level: 2, bars: 3, color: '#f59e0b', key: 'fair' },
|
|||
|
|
{ min: 30, level: 1, bars: 2, color: '#f97316', key: 'weak' },
|
|||
|
|
]
|
|||
|
|
const EXPECTED_NONE = { level: 0, bars: 1, color: '#9ca3af', key: 'none' }
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 只取契约字段:`rssiSignal()` 返回 `{level,bars,color,key}`,
|
|||
|
|
* **不含** `RSSI_TIERS` 里的 `min`(阈值是实现细节,不进返回值)。
|
|||
|
|
*/
|
|||
|
|
function contract({ level, bars, color, key }) {
|
|||
|
|
return { level, bars, color, key }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 权威分档表的契约形态(供边界断言直接用) */
|
|||
|
|
const T = EXPECTED_TIERS.map(contract)
|
|||
|
|
|
|||
|
|
/** 该值按权威契约应落在哪一档 */
|
|||
|
|
function expectedFor(rssi) {
|
|||
|
|
if (rssi === null || rssi === undefined) return EXPECTED_NONE
|
|||
|
|
const value = Number(rssi)
|
|||
|
|
if (!Number.isFinite(value)) return EXPECTED_NONE
|
|||
|
|
for (const tier of EXPECTED_TIERS) {
|
|||
|
|
if (value >= tier.min) return contract(tier)
|
|||
|
|
}
|
|||
|
|
return EXPECTED_NONE
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// 1. 两端一致性:遍历 0–99(协议全量程)逐值断言
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
test('test_web_and_mp_rssi_signal_agree_over_0_99', () => {
|
|||
|
|
for (let rssi = 0; rssi <= 99; rssi++) {
|
|||
|
|
assert.deepEqual(
|
|||
|
|
web.rssiSignal(rssi),
|
|||
|
|
mp.rssiSignal(rssi),
|
|||
|
|
`rssi=${rssi} 两端 rssiSignal 不一致`,
|
|||
|
|
)
|
|||
|
|
assert.deepEqual(
|
|||
|
|
web.rssiBarColors(rssi),
|
|||
|
|
mp.rssiBarColors(rssi),
|
|||
|
|
`rssi=${rssi} 两端 rssiBarColors 不一致`,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
test('test_web_and_mp_rssi_agree_on_missing_and_boundary_values', () => {
|
|||
|
|
const values = [null, undefined, Number.NaN, Number.POSITIVE_INFINITY, -1, 29, 30, 39, 40, 54, 55, 69, 70, 99, 100, 255]
|
|||
|
|
for (const value of values) {
|
|||
|
|
assert.deepEqual(web.rssiSignal(value), mp.rssiSignal(value), `rssi=${String(value)} 两端不一致`)
|
|||
|
|
assert.deepEqual(
|
|||
|
|
web.rssiBarColors(value),
|
|||
|
|
mp.rssiBarColors(value),
|
|||
|
|
`rssi=${String(value)} 两端 rssiBarColors 不一致`,
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
test('test_web_and_mp_rssi_tier_tables_are_identical', () => {
|
|||
|
|
assert.deepEqual(web.RSSI_TIERS, mp.RSSI_TIERS, '两端 RSSI_TIERS 必须逐字一致')
|
|||
|
|
assert.deepEqual(web.RSSI_NONE, mp.RSSI_NONE, '两端 RSSI_NONE 必须一致')
|
|||
|
|
assert.equal(web.RSSI_MAX_BARS, mp.RSSI_MAX_BARS, '两端 RSSI_MAX_BARS 必须一致')
|
|||
|
|
assert.equal(web.RSSI_MAX_BARS, 5, '统一 5 格制')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// 2. 契约本身:档位边界 + 颜色表(防止"统一"成另一套阈值而无人发现)
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
test('test_rssi_tier_boundaries_match_agreed_contract', () => {
|
|||
|
|
const boundaries = [
|
|||
|
|
[-1, EXPECTED_NONE],
|
|||
|
|
[0, EXPECTED_NONE],
|
|||
|
|
[29, EXPECTED_NONE],
|
|||
|
|
[30, T[3]],
|
|||
|
|
[39, T[3]],
|
|||
|
|
[40, T[2]],
|
|||
|
|
[54, T[2]],
|
|||
|
|
[55, T[1]],
|
|||
|
|
[69, T[1]],
|
|||
|
|
[70, T[0]],
|
|||
|
|
[99, T[0]],
|
|||
|
|
]
|
|||
|
|
for (const [rssi, expected] of boundaries) {
|
|||
|
|
assert.deepEqual(web.rssiSignal(rssi), expected, `rssi=${rssi} 档位不符契约`)
|
|||
|
|
}
|
|||
|
|
// 无数据 / 非数字一律落"无信号"档(灰色底格),不得猜测成"信号好"
|
|||
|
|
for (const bad of [null, undefined, Number.NaN]) {
|
|||
|
|
assert.deepEqual(web.rssiSignal(bad), EXPECTED_NONE, `rssi=${String(bad)} 必须落无信号档`)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
test('test_rssi_colors_match_agreed_contract', () => {
|
|||
|
|
// 收敛后的配色:绿 → 蓝 → 黄 → 橙 → 灰(**注意 ≥55 是蓝、30–39 是橙而非红**)
|
|||
|
|
assert.deepEqual(
|
|||
|
|
[99, 60, 45, 33, 10].map((v) => web.rssiSignal(v).color),
|
|||
|
|
['#16a34a', '#2563eb', '#f59e0b', '#f97316', '#9ca3af'],
|
|||
|
|
)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
test('test_rssi_bars_increase_with_level_and_stay_within_max', () => {
|
|||
|
|
let prev = 0
|
|||
|
|
// 逐档递增,且 1..5 全部落在 5 格制内
|
|||
|
|
for (const rssi of [10, 30, 40, 55, 70]) {
|
|||
|
|
const { bars } = web.rssiSignal(rssi)
|
|||
|
|
assert.ok(bars > prev, `rssi=${rssi} 的格数应比上一档多(bars=${bars})`)
|
|||
|
|
assert.ok(bars >= 1 && bars <= web.RSSI_MAX_BARS, `rssi=${rssi} 格数越界:${bars}`)
|
|||
|
|
prev = bars
|
|||
|
|
}
|
|||
|
|
// 满档 = 5 格;离线也是 1 格底格(不是 0 格 —— 0 格会让图标"消失")
|
|||
|
|
assert.equal(web.rssiSignal(70).bars, 5)
|
|||
|
|
assert.equal(web.rssiSignal(null).bars, 1)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
test('test_rssi_bar_colors_length_equals_bars_and_single_color', () => {
|
|||
|
|
for (let rssi = 0; rssi <= 99; rssi++) {
|
|||
|
|
const signal = web.rssiSignal(rssi)
|
|||
|
|
const colors = web.rssiBarColors(rssi)
|
|||
|
|
assert.equal(colors.length, signal.bars, `rssi=${rssi} 颜色数组长度必须等于格数`)
|
|||
|
|
assert.ok(colors.every((c) => c === signal.color), `rssi=${rssi} 每格必须同色`)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// 3. 反证:把"收敛前的两端不一致"作为反例 —— 若谁把某一端改回去,本组会红
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
test('test_mp_no_longer_uses_the_old_four_tier_red_30_39_scale', () => {
|
|||
|
|
// 旧小程序口径:30–39 → 红;且没有 ≥70 档(55 就是最高)
|
|||
|
|
// 收敛后:30–39 必须是**橙** `#f97316`,且 70 必须是**独立的一档**(绿,5 格)
|
|||
|
|
assert.equal(mp.rssiSignal(35).color, '#f97316', '小程序 30–39 必须是橙色(旧实现是红 #ef4444)')
|
|||
|
|
assert.notEqual(mp.rssiSignal(35).color, '#ef4444', '不得再出现旧的红色 30–39 档')
|
|||
|
|
assert.notEqual(mp.rssiSignal(80).color, mp.rssiSignal(60).color, '≥70 必须是独立档(不与 ≥55 同色)')
|
|||
|
|
assert.equal(mp.rssiSignal(80).bars, 5, '≥70 应为满格 5 格')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
test('test_bars_are_not_constant_five_for_all_levels', () => {
|
|||
|
|
// 收敛前的 bug:小程序恒渲染 5 格、只改颜色 ⇒ 所有档位 bars 都是 5。
|
|||
|
|
// 现在格数必须随档位变化(否则本用例红)。
|
|||
|
|
const barsSet = new Set([10, 30, 40, 55, 70].map((v) => mp.rssiSignal(v).bars))
|
|||
|
|
assert.ok(barsSet.size >= 4, `格数必须随档位变化,实际集合:${[...barsSet].join(',')}`)
|
|||
|
|
assert.notEqual(mp.rssiSignal(10).bars, mp.rssiSignal(70).bars, '离线档与满档格数必须不同')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// 运行
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
let failed = 0
|
|||
|
|
for (const [name, fn] of cases) {
|
|||
|
|
try {
|
|||
|
|
fn()
|
|||
|
|
console.log(`ok ${name}`)
|
|||
|
|
} catch (err) {
|
|||
|
|
failed += 1
|
|||
|
|
console.error(`FAIL ${name}\n ${err.message}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
console.log(`\n${cases.length - failed} passed, ${failed} failed (共 ${cases.length} 个用例)`)
|
|||
|
|
process.exit(failed === 0 ? 0 : 1)
|