119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { MemoryRouter } from 'react-router-dom'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type * as HermesApi from '@/hermes'
|
|
import { queryClient } from '@/lib/query-client'
|
|
|
|
const getSkills = vi.fn()
|
|
const getToolsets = vi.fn()
|
|
const toggleSkill = vi.fn()
|
|
const toggleToolset = vi.fn()
|
|
const getToolsetConfig = vi.fn()
|
|
const selectToolsetProvider = vi.fn()
|
|
const getUsageAnalytics = vi.fn()
|
|
|
|
// Partial mock: keep the real module (SkillsView pulls in @/store/profile,
|
|
// whose import-time subscription calls setApiRequestProfile) and stub only the
|
|
// calls we assert on.
|
|
vi.mock('@/hermes', async importOriginal => ({
|
|
...(await importOriginal<typeof HermesApi>()),
|
|
getSkills: () => getSkills(),
|
|
getToolsets: () => getToolsets(),
|
|
toggleSkill: (name: string, enabled: boolean) => toggleSkill(name, enabled),
|
|
toggleToolset: (name: string, enabled: boolean) => toggleToolset(name, enabled),
|
|
getToolsetConfig: (name: string) => getToolsetConfig(name),
|
|
selectToolsetProvider: (toolset: string, provider: string) => selectToolsetProvider(toolset, provider),
|
|
getUsageAnalytics: (days: number) => getUsageAnalytics(days)
|
|
}))
|
|
|
|
// Notifications hit nanostores/timers we don't care about here.
|
|
vi.mock('@/store/notifications', () => ({
|
|
notify: vi.fn(),
|
|
notifyError: vi.fn()
|
|
}))
|
|
|
|
function toolset(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
name: 'web',
|
|
label: 'Web Search',
|
|
description: 'web_search, web_extract',
|
|
enabled: true,
|
|
available: true,
|
|
configured: true,
|
|
tools: ['web_search', 'web_extract'],
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
async function renderSkills() {
|
|
const { SkillsView } = await import('./index')
|
|
let result: ReturnType<typeof render>
|
|
await act(async () => {
|
|
result = render(
|
|
// SkillsView reads skills/toolsets via useQuery, so it needs a provider.
|
|
<QueryClientProvider client={queryClient}>
|
|
<MemoryRouter initialEntries={['/skills?tab=toolsets']}>
|
|
<SkillsView />
|
|
</MemoryRouter>
|
|
</QueryClientProvider>
|
|
)
|
|
})
|
|
|
|
return result!
|
|
}
|
|
|
|
beforeEach(() => {
|
|
getSkills.mockResolvedValue([])
|
|
getToolsets.mockResolvedValue([toolset()])
|
|
toggleToolset.mockResolvedValue({ ok: true, name: 'web', enabled: false })
|
|
getToolsetConfig.mockResolvedValue({ has_category: true, active_provider: null, providers: [] })
|
|
getUsageAnalytics.mockResolvedValue({ tools: [] })
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.clearAllMocks()
|
|
// Shared singleton client — drop cached skills/toolsets so each test refetches.
|
|
queryClient.clear()
|
|
})
|
|
|
|
describe('SkillsView toolset management', () => {
|
|
it('renders a switch for each toolset and toggles it off', async () => {
|
|
await renderSkills()
|
|
|
|
const sw = await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
|
|
expect(sw.getAttribute('aria-checked')).toBe('true')
|
|
|
|
await act(async () => {
|
|
fireEvent.click(sw)
|
|
})
|
|
|
|
await waitFor(() => expect(toggleToolset).toHaveBeenCalledWith('web', false))
|
|
})
|
|
|
|
it('renders toolset titles without leading emoji', async () => {
|
|
getToolsets.mockResolvedValue([toolset({ name: 'cronjob', label: '⏰ Cron Jobs', description: 'cron tools' })])
|
|
|
|
await renderSkills()
|
|
|
|
// The label renders in both the row and the auto-selected detail header, so
|
|
// assert via the switch's (emoji-stripped) accessible name and the absence
|
|
// of the emoji rather than a single-match text lookup.
|
|
await screen.findByRole('switch', { name: 'Toggle Cron Jobs toolset' })
|
|
expect(screen.queryByText(/⏰/)).toBeNull()
|
|
})
|
|
|
|
it('renders the provider config panel inline for the selected toolset', async () => {
|
|
// The master-detail UI dropped the resting "Configured" pill and the
|
|
// "Configure" expander: the detail column auto-selects the first toolset
|
|
// and renders its config panel directly, which fetches on mount.
|
|
await renderSkills()
|
|
|
|
await screen.findByRole('switch', { name: 'Toggle Web Search toolset' })
|
|
await waitFor(() => expect(getToolsetConfig).toHaveBeenCalledWith('web'))
|
|
})
|
|
})
|