feat(web): combine plugin settings into tabs
This commit is contained in:
@@ -8,6 +8,9 @@ import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { SettingsScopeService } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { apply, inject } from '@deepseek-ai/dsh-client-ui-plugin-config/client'
|
||||
import type {
|
||||
ConfigurablePluginsTabInjected, PluginConfigSectionInjected,
|
||||
} from '@deepseek-ai/dsh-client-ui-plugin-config/client'
|
||||
|
||||
// The service reads its initial locale from the browser; these specs assert
|
||||
// the shipped Chinese copy, so they state the browser they assume.
|
||||
@@ -46,16 +49,20 @@ describe('ui-plugin-config apply', () => {
|
||||
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope'])
|
||||
})
|
||||
|
||||
it('registers the section and declares the per-plugin card slot', async () => {
|
||||
it('registers one Plugins section and declares the tab and card slots', async () => {
|
||||
const { ctx, slots } = await bench()
|
||||
declareRoot(slots)
|
||||
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
|
||||
const section = slots.entries('settings.section')[0]!
|
||||
expect(section.options).toMatchObject({ id: 'plugins', order: 30 })
|
||||
expect(section.options).toMatchObject({ id: 'plugins', order: 15 })
|
||||
// The nav label is a locale-following thunk; owners resolve it at read time.
|
||||
expect(resolveSlotLabel(section.options.label)).toBe('插件配置')
|
||||
expect(resolveSlotLabel(section.options.label)).toBe('插件')
|
||||
expect(slots.spec('settings.plugins.tab')).toMatchObject({ kind: 'list', scope: 'root' })
|
||||
const tab = slots.entries('settings.plugins.tab')[0]!
|
||||
expect(tab.options).toMatchObject({ id: 'configurable', order: 0 })
|
||||
expect(resolveSlotLabel(tab.options.label)).toBe('插件配置')
|
||||
expect(slots.spec('settings.plugin.item')).toMatchObject({ kind: 'list', scope: 'root' })
|
||||
})
|
||||
|
||||
@@ -69,13 +76,18 @@ describe('ui-plugin-config apply', () => {
|
||||
.toEqual(['bash', 'agent-loop', 'web-search'])
|
||||
})
|
||||
|
||||
it('injects a live card count and one business face per card', async () => {
|
||||
it('injects a live tab projection, a card count, and one business face per card', async () => {
|
||||
const { ctx, slots } = await bench()
|
||||
declareRoot(slots)
|
||||
await ctx.plugin({ inject: [...inject], apply }).await()
|
||||
|
||||
const section = slots.entries('settings.section')[0]!
|
||||
expect((section as { inject?: () => unknown }).inject?.()).toEqual({ cardCount: 3 })
|
||||
const sectionFace = (section.inject as unknown as () => PluginConfigSectionInjected)()
|
||||
expect(sectionFace.hooks.tabs.getSnapshot()).toEqual([
|
||||
{ id: 'configurable', order: 0, label: '插件配置' },
|
||||
])
|
||||
const tab = slots.entries('settings.plugins.tab')[0]!
|
||||
expect((tab.inject as unknown as () => ConfigurablePluginsTabInjected)()).toEqual({ cardCount: 3 })
|
||||
for (const entry of slots.entries('settings.plugin.item')) {
|
||||
const face = (entry as { inject?: () => unknown }).inject?.() as { hooks: Record<string, unknown> }
|
||||
// Each card injects exactly one snapshot store plus its own actions.
|
||||
@@ -129,6 +141,7 @@ describe('ui-plugin-config apply', () => {
|
||||
await fiber.dispose()
|
||||
|
||||
expect(slots.entries('settings.section')).toHaveLength(0)
|
||||
expect(slots.spec('settings.plugins.tab')).toBeUndefined()
|
||||
expect(slots.spec('settings.plugin.item')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,8 +13,10 @@ import { AgentLoopCard } from '../src/client/AgentLoopCard.tsx'
|
||||
import type { AgentLoopCardProps } from '../src/client/AgentLoopCard.tsx'
|
||||
import { BashCard } from '../src/client/BashCard.tsx'
|
||||
import type { BashCardProps } from '../src/client/BashCard.tsx'
|
||||
import { ConfigurablePluginsTab } from '../src/client/ConfigurablePluginsTab.tsx'
|
||||
import type { ConfigurablePluginsTabProps } from '../src/client/ConfigurablePluginsTab.tsx'
|
||||
import { PluginConfigSection } from '../src/client/PluginConfigSection.tsx'
|
||||
import type { PluginConfigSectionProps } from '../src/client/PluginConfigSection.tsx'
|
||||
import type { PluginConfigSectionProps, PluginSettingsTabRow } from '../src/client/PluginConfigSection.tsx'
|
||||
import { WebSearchCard } from '../src/client/WebSearchCard.tsx'
|
||||
import type { WebSearchCardProps } from '../src/client/WebSearchCard.tsx'
|
||||
import type { AgentLoopCardState } from '../src/client/agent-loop-store.ts'
|
||||
@@ -46,13 +48,24 @@ function cardActions() {
|
||||
return { edit: vi.fn(), resetField: vi.fn(), save: vi.fn(), discard: vi.fn() }
|
||||
}
|
||||
|
||||
function renderSection(cardCount: number, cards = 'cards') {
|
||||
function renderSection(rows: readonly PluginSettingsTabRow[]) {
|
||||
const props = {
|
||||
t,
|
||||
useTabs: (selector: (value: readonly PluginSettingsTabRow[]) => unknown) => selector(rows),
|
||||
renderSlot: (_name: string, _owner: unknown, options: { only?: string }) => (
|
||||
<span>{options.only}</span>
|
||||
),
|
||||
} as unknown as PluginConfigSectionProps
|
||||
render(<PluginConfigSection {...props} />)
|
||||
}
|
||||
|
||||
function renderConfigurable(cardCount: number, cards = 'cards') {
|
||||
const props = {
|
||||
t,
|
||||
cardCount,
|
||||
renderSlot: () => <li>{cards}</li>,
|
||||
} as unknown as PluginConfigSectionProps
|
||||
render(<PluginConfigSection {...props} />)
|
||||
} as unknown as ConfigurablePluginsTabProps
|
||||
render(<ConfigurablePluginsTab {...props} />)
|
||||
}
|
||||
|
||||
function renderBash(state: Partial<BashCardState> = {}) {
|
||||
@@ -69,26 +82,53 @@ function renderBash(state: Partial<BashCardState> = {}) {
|
||||
}
|
||||
|
||||
describe('PluginConfigSection', () => {
|
||||
it('says so when no plugin contributed a tab', () => {
|
||||
renderSection([])
|
||||
|
||||
expect(screen.getByText(en.empty)).toBeTruthy()
|
||||
expect(screen.queryByRole('tab')).toBeNull()
|
||||
})
|
||||
|
||||
it('defaults to the first ordered tab and mounts another only after selection', () => {
|
||||
renderSection([
|
||||
{ id: 'configurable', order: 0, label: en.configurableTab },
|
||||
{ id: 'all', order: 10, label: 'Plugin list' },
|
||||
])
|
||||
|
||||
const configurable = screen.getByRole('tab', { name: en.configurableTab })
|
||||
const all = screen.getByRole('tab', { name: 'Plugin list' })
|
||||
expect(configurable.getAttribute('aria-selected')).toBe('true')
|
||||
expect(screen.getByText('configurable')).toBeTruthy()
|
||||
expect(screen.queryByText('all')).toBeNull()
|
||||
|
||||
fireEvent.click(all)
|
||||
expect(all.getAttribute('aria-selected')).toBe('true')
|
||||
expect(screen.getByText('all')).toBeTruthy()
|
||||
expect(screen.getByText('configurable').closest('[role="tabpanel"]')).toHaveProperty('hidden', true)
|
||||
})
|
||||
|
||||
it('leads with its own heading and intro', () => {
|
||||
renderSection([{ id: 'configurable', order: 0, label: en.configurableTab }])
|
||||
|
||||
expect(screen.getByRole('heading', { name: en.title })).toBeTruthy()
|
||||
expect(screen.getByText(en.intro)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ConfigurablePluginsTab', () => {
|
||||
it('says so when no plugin contributed a card', () => {
|
||||
renderSection(0)
|
||||
renderConfigurable(0)
|
||||
|
||||
expect(screen.getByText(en.empty)).toBeTruthy()
|
||||
expect(screen.queryByText('cards')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders the card list once a plugin contributed one', () => {
|
||||
renderSection(1)
|
||||
renderConfigurable(1)
|
||||
|
||||
expect(screen.getByText('cards')).toBeTruthy()
|
||||
expect(screen.queryByText(en.empty)).toBeNull()
|
||||
})
|
||||
|
||||
it('leads with its own heading and intro', () => {
|
||||
renderSection(1)
|
||||
|
||||
expect(screen.getByRole('heading', { name: en.title })).toBeTruthy()
|
||||
expect(screen.getByText(en.intro)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('BashCard', () => {
|
||||
|
||||
Reference in New Issue
Block a user