新增仪表盘页:流量曲线、流量与内存数字、配置概览、内核状态
流量曲线手画 SVG polyline 而不引图表库:60 个点两条线,一个图表库几百 KB, 决策记录 0006 留的例外用在这里不划算。/traffic 和 /memory 是永不结束的流, api.request() 会把整个响应读完等于挂死,所以新增 api.openStream() 返回响应体, 交给 web/stream.ts 的 readLines 逐行读,离开页面中止 fetch 断流。 导航入口要在 App.vue 里挂,按约定留给集成者统一改。
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { WINDOW, chartPeak, kernelPorts, linePoints, modeLabel, pushSample } from './dashboard.ts'
|
||||
import type { TrafficSample } from './dashboard.ts'
|
||||
|
||||
const sample = (up: number, down: number): TrafficSample => ({
|
||||
up,
|
||||
down,
|
||||
upTotal: 0,
|
||||
downTotal: 0,
|
||||
})
|
||||
|
||||
describe('流量样本窗口', () => {
|
||||
it('追加样本', () => {
|
||||
expect(pushSample([sample(1, 2)], sample(3, 4))).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('窗口满了挤掉最旧的', () => {
|
||||
const full = Array.from({ length: WINDOW }, (_, i) => sample(i, i))
|
||||
const next = pushSample(full, sample(999, 999))
|
||||
expect(next).toHaveLength(WINDOW)
|
||||
expect(next.at(-1)?.up).toBe(999)
|
||||
expect(next[0]?.up).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('纵轴上限', () => {
|
||||
it('空窗口和全 0 都有下限 1,不会除出 NaN', () => {
|
||||
expect(chartPeak([])).toBe(1)
|
||||
expect(chartPeak([sample(0, 0)])).toBe(1)
|
||||
})
|
||||
|
||||
it('取上行下行的共同峰值', () => {
|
||||
expect(chartPeak([sample(10, 500), sample(800, 20)])).toBe(800)
|
||||
})
|
||||
})
|
||||
|
||||
describe('折线坐标', () => {
|
||||
it('最新样本钉在右缘,最旧的往左排', () => {
|
||||
const points = linePoints([5, 10], 10, 590, 100).split(' ')
|
||||
expect(points).toHaveLength(2)
|
||||
// 窗口 60,步长 10:两条样本占最右边两格
|
||||
expect(points[0]).toBe('580.0,50.0')
|
||||
expect(points[1]).toBe('590.0,0.0')
|
||||
})
|
||||
|
||||
it('窗口不满不拉伸,左边留空', () => {
|
||||
const points = linePoints([7], 10, 590, 100).split(' ')
|
||||
expect(points).toEqual(['590.0,30.0'])
|
||||
})
|
||||
|
||||
it('全是 0 时整条贴在底线上', () => {
|
||||
for (const point of linePoints([0, 0, 0], 1, 590, 100).split(' ')) {
|
||||
expect(point.endsWith(',100.0')).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('运行模式', () => {
|
||||
it('三个已知模式有中文名', () => {
|
||||
expect(modeLabel('rule')).toBe('规则')
|
||||
expect(modeLabel('global')).toBe('全局')
|
||||
expect(modeLabel('direct')).toBe('直连')
|
||||
})
|
||||
|
||||
it('没见过的值原样显示,缺省是破折号', () => {
|
||||
expect(modeLabel('script')).toBe('script')
|
||||
expect(modeLabel(undefined)).toBe('—')
|
||||
expect(modeLabel('')).toBe('—')
|
||||
})
|
||||
})
|
||||
|
||||
describe('代理端口', () => {
|
||||
it('挑出大于 0 的端口,按固定顺序', () => {
|
||||
expect(kernelPorts({ 'mixed-port': 7890, port: 7891, 'socks-port': 0 })).toEqual([
|
||||
{ name: 'port', port: 7891 },
|
||||
{ name: 'mixed-port', port: 7890 },
|
||||
])
|
||||
})
|
||||
|
||||
it('0、缺省和非数字都不算启用', () => {
|
||||
expect(kernelPorts({ port: 0, 'socks-port': 'abc' })).toEqual([])
|
||||
})
|
||||
|
||||
it('字符串端口号也认得', () => {
|
||||
expect(kernelPorts({ port: '7890' })).toEqual([{ name: 'port', port: 7890 }])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user