右侧详情面板取代原来的手风琴展开:空间用起来了,选中状态更清楚,行也不会被推来推去。 出站选择做成分组的按钮网格而不是下拉框——一眼看得到全部可选项和当前状态。 点一下就走完整的落盘链路(校验、备份、写入、重载),失败时按停在哪一步显示不同的话。 订阅线路那一档暂时禁用:选中它需要先自动建一个筛选组,那部分还没做。 顺带两处从截图里看出来的问题:连接线原本是固定 40px 的短横,读起来像装饰不像连接, 改成占据弹性列真的把两头连起来;默认入站的名字原本是「HTTP 和 SOCKS5 共用」, 和类型列重复,改成它来自的配置键 mixed-port。
87 lines
3.4 KiB
TypeScript
87 lines
3.4 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { openDocument } from '../vendor/config-editor/index.ts'
|
|
import { collectInbounds, collectOutboundNames, outboundToProxyField } from './inbounds.ts'
|
|
|
|
const fixture = openDocument(
|
|
readFileSync(join(import.meta.dir, '__fixtures__', 'config-with-comments.yaml'), 'utf8'),
|
|
).data
|
|
|
|
describe('合并两套写法的入站', () => {
|
|
it('只有旧写法时也能列出来,不会是空表', () => {
|
|
const list = collectInbounds(fixture)
|
|
expect(list).toHaveLength(1)
|
|
expect(list[0]!.port).toBe('7777')
|
|
expect(list[0]!.type).toBe('mixed')
|
|
expect(list[0]!.isDefault).toBe(true)
|
|
expect(list[0]!.legacyKey).toBe('mixed-port')
|
|
})
|
|
|
|
it('端口写 0 表示没启用,不该出现在表里', () => {
|
|
const list = collectInbounds({ 'mixed-port': 7777, 'socks-port': 0, port: 0 })
|
|
expect(list.map((i) => i.legacyKey)).toEqual(['mixed-port'])
|
|
})
|
|
|
|
it('新旧两套同时存在时都要列出来', () => {
|
|
const list = collectInbounds({
|
|
'mixed-port': 7777,
|
|
listeners: [{ name: '容器A', type: 'mixed', port: 7778, proxy: 'AUTO' }],
|
|
})
|
|
expect(list.map((i) => i.name)).toEqual(['mixed-port', '容器A'])
|
|
expect(list[0]!.isDefault).toBe(true)
|
|
expect(list[1]!.isDefault).toBe(false)
|
|
expect(list[1]!.listenerIndex).toBe(0)
|
|
})
|
|
|
|
it('默认入站的出站永远是「跟着规则走」,因为旧写法没处写', () => {
|
|
expect(collectInbounds({ 'mixed-port': 7777 })[0]!.outbound).toEqual({ kind: 'rules' })
|
|
})
|
|
})
|
|
|
|
describe('出站去向的四种形态', () => {
|
|
const of = (proxy?: string) => {
|
|
// 用扩展而不是直接写 proxy: undefined —— 配置里「没有这个键」和「这个键是空」
|
|
// 是两回事,测试要还原真实的文件形状。
|
|
const listener = { name: 'x', type: 'mixed', port: 1, ...(proxy === undefined ? {} : { proxy }) }
|
|
return collectInbounds({ listeners: [listener] })[0]!.outbound
|
|
}
|
|
|
|
it('没写 proxy 就是跟着规则走', () => {
|
|
expect(of()).toEqual({ kind: 'rules' })
|
|
expect(of('')).toEqual({ kind: 'rules' })
|
|
})
|
|
|
|
it('DIRECT 和 GLOBAL 是内核自带的,单独成一类', () => {
|
|
expect(of('DIRECT')).toEqual({ kind: 'direct' })
|
|
expect(of('GLOBAL')).toEqual({ kind: 'global' })
|
|
})
|
|
|
|
it('其余都是指名道姓的出站', () => {
|
|
expect(of('AUTO')).toEqual({ kind: 'named', name: 'AUTO' })
|
|
})
|
|
|
|
it('写回时能还原成原来的字段值', () => {
|
|
expect(outboundToProxyField({ kind: 'rules' })).toBeUndefined()
|
|
expect(outboundToProxyField({ kind: 'direct' })).toBe('DIRECT')
|
|
expect(outboundToProxyField({ kind: 'global' })).toBe('GLOBAL')
|
|
expect(outboundToProxyField({ kind: 'named', name: 'AUTO' })).toBe('AUTO')
|
|
})
|
|
})
|
|
|
|
describe('能被入站引用的出站名字', () => {
|
|
it('列出内置出站、策略组和自有线路', () => {
|
|
const names = collectOutboundNames(fixture)
|
|
expect(names.groups).toEqual(['PROXY', 'AUTO'])
|
|
expect(names.builtins).toContain('DIRECT')
|
|
expect(names.builtins).toContain('GLOBAL')
|
|
})
|
|
|
|
it('订阅带来的线路不在其中——它们不在内核的名字表上', () => {
|
|
const names = collectOutboundNames(fixture)
|
|
// 样本里唯一的订阅是 subs,它带来的线路无法被按名字引用
|
|
expect(names.ownProxies).toEqual([])
|
|
expect([...names.groups, ...names.ownProxies, ...names.builtins]).not.toContain('subs')
|
|
})
|
|
})
|