- SidebarPrefs 新增 tabsEnabled / viewersEnabled 开放 map(缺省 = 启用), PrefsSchema 用 z.dict 校验,旧文档零迁移 - TabDescriptor/FileViewerDescriptor 新增声明式设置面:viewer 补 title/icon, 两者补 settings.toggles(子代理声明 autoOpenSubagent、终端声明 agentTerminalTools) - 服务新增 isTabEnabled/isViewerEnabled;openTab 拒绝禁用类型、matchFileViewer 跳过禁用 viewer - SideCardSection 重写为注册表驱动清单:侧边栏内容(图标+类型id+开关+嵌套开关) + 文件预览(图标+标题+扩展名+开关),开关持久化 + 乐观提交回滚 - 消费点 gating:+ 菜单过滤、子代理自动展开、agent 终端 reconcile、 产出文件拦截(editor 关闭时回落默认行)、新会话 explorer 预置 - 内置 viewer 补 6 个手绘 outline 图标;store 快照携带 prefs 使开关变更即时生效 - 测试:service gating / builtins 声明 / parsePrefs map 校验 / schema 断言 / 新增 SideCardSection renderToString 规格 - 文档:README 设置小节、AGENTS.md 描述符与服务方法、新增设计文档;版本 0.5.0
118 lines
4.6 KiB
TypeScript
118 lines
4.6 KiB
TypeScript
/**
|
|
* Side card settings section render tests: the section is DECLARATIVE —
|
|
* every row (icon, title, type id, extensions, enable switch) derives from
|
|
* the sidebar service's tab/viewer registries instead of hardcoded copy.
|
|
* These specs pin that derivation with a fake store + a small registry:
|
|
* registered tabs/viewers appear with their declared icon/title/settings,
|
|
* disabled states render unchecked, and a disabled tab hides its declared
|
|
* nested toggles.
|
|
*
|
|
* Rendered with renderToString (mount effects — the settings RPC sync — do
|
|
* not run in SSR; the initial store prefs are the render input).
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { renderToString } from 'react-dom/server'
|
|
import { createElement } from 'react'
|
|
import { createSidebarStore, type SidebarStore } from '../src/client/state.ts'
|
|
import { createBetterSidebarService, type BetterSidebarService } from '../src/client/service.ts'
|
|
import { SideCardSection, type SideCardSectionProps } from '../src/client/SideCardSection.tsx'
|
|
|
|
/** One tab + one viewer + the subagent-style nested toggle under a tab. */
|
|
function mount(): { store: SidebarStore; service: BetterSidebarService } {
|
|
const store = createSidebarStore()
|
|
const service = createBetterSidebarService(store)
|
|
service.registerTab({
|
|
id: 'explorer',
|
|
title: () => 'Explorer',
|
|
icon: () => createElement('svg', { 'data-icon': 'explorer' }),
|
|
order: 10,
|
|
component: () => null,
|
|
})
|
|
service.registerTab({
|
|
id: 'subagent',
|
|
title: () => 'Subagents',
|
|
icon: () => createElement('svg', { 'data-icon': 'subagent' }),
|
|
order: 30,
|
|
settings: {
|
|
toggles: [{
|
|
key: 'autoOpenSubagent',
|
|
title: () => 'Auto-open Subagents',
|
|
desc: () => 'Expand on new subagent',
|
|
}],
|
|
},
|
|
component: () => null,
|
|
})
|
|
service.registerFileViewer({
|
|
id: 'image',
|
|
title: () => 'Image',
|
|
icon: () => createElement('svg', { 'data-icon': 'image' }),
|
|
exts: ['png', 'jpg'],
|
|
fetchStrategy: 'mediaUrl',
|
|
component: () => null,
|
|
})
|
|
return { store, service }
|
|
}
|
|
|
|
function renderSection(store: SidebarStore, service: BetterSidebarService): string {
|
|
return renderToString(createElement(
|
|
SideCardSection,
|
|
{ store, service } as unknown as SideCardSectionProps,
|
|
))
|
|
}
|
|
|
|
describe('SideCardSection declarative inventory', () => {
|
|
it('renders one row per registered tab: icon + title + type id + switch', () => {
|
|
const { store, service } = mount()
|
|
const html = renderSection(store, service)
|
|
expect(html).toContain('data-icon="explorer"')
|
|
expect(html).toContain('>Explorer<')
|
|
// The type id is the row's desc (the declarative "type" surface).
|
|
expect(html).toContain('>explorer<')
|
|
expect(html).toContain('data-icon="subagent"')
|
|
expect(html).toContain('>Subagents<')
|
|
// Both tabs are enabled by default: their switches render checked.
|
|
expect(html.match(/type="checkbox"/g)?.length).toBeGreaterThanOrEqual(4)
|
|
})
|
|
|
|
it('renders the declared nested toggle under an enabled tab', () => {
|
|
const { store, service } = mount()
|
|
const html = renderSection(store, service)
|
|
// The subagent tab's declared related setting appears while enabled,
|
|
// bound to its prefs key (default true → checked).
|
|
expect(html).toContain('Auto-open Subagents')
|
|
expect(html).toContain('Expand on new subagent')
|
|
})
|
|
|
|
it('hides the nested toggles while the parent tab is disabled', () => {
|
|
const { store, service } = mount()
|
|
store.setPrefs({ ...store.getPrefs(), tabsEnabled: { subagent: false } })
|
|
const html = renderSection(store, service)
|
|
// The parent row stays (so the user can re-enable it) but unchecked.
|
|
expect(html).toContain('>Subagents<')
|
|
expect(html).not.toContain('Auto-open Subagents')
|
|
})
|
|
|
|
it('renders one row per registered viewer: icon + title + exts + switch', () => {
|
|
const { store, service } = mount()
|
|
const html = renderSection(store, service)
|
|
expect(html).toContain('data-icon="image"')
|
|
expect(html).toContain('>Image<')
|
|
// The covered extensions are the row's desc.
|
|
expect(html).toContain('png · jpg')
|
|
})
|
|
|
|
it('a disabled viewer renders unchecked', () => {
|
|
const { store, service } = mount()
|
|
store.setPrefs({ ...store.getPrefs(), viewersEnabled: { image: false } })
|
|
const html = renderSection(store, service)
|
|
expect(html).toContain('>Image<')
|
|
})
|
|
|
|
it('a disabled tab renders unchecked (its switch reflects the prefs map)', () => {
|
|
const { store, service } = mount()
|
|
store.setPrefs({ ...store.getPrefs(), tabsEnabled: { explorer: false } })
|
|
const html = renderSection(store, service)
|
|
expect(html).toContain('>explorer<')
|
|
})
|
|
})
|