import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { useState } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { describe, expect, it, vi } from 'vitest'; import { PluginsView } from '@/pages/Plugins'; import type { PluginWorkspaceFilters, PluginWorkspaceItem, PluginWorkspaceProjection, } from '@/pages/Plugins/plugin-workspace-model'; const filters: PluginWorkspaceFilters = { scope: 'project', source: 'all', state: 'all', search: '', selectedKey: null, }; const projectPlugin = { id: 'makelore.data-service', version: '1.0.0', contractVersion: 1, requiresBackend: true, displayName: '开发数据服务', description: '为当前项目提供 JSON 数据。', enabled: true, state: 'configuration_required' as const, backend: { status: 'unconfigured' as const }, skills: [{ id: 'data-service', assignedAgentIds: ['agent-a'] }], capabilities: [{ id: 'data-service.documents', operations: [{ id: 'put_document', billing: { mode: 'included' as const, availability: 'available' as const, notice: 'Fixed quotas apply' }, tool: { name: 'data_service_put_document', label: '写入文档', description: '写入项目数据', mutation: 'write' as const, permissions: ['project.data.write'], }, }], }], settingsSurface: 'data-service', }; const officialItem: PluginWorkspaceItem = { key: 'official:makelore.data-service', source: 'official', pluginId: 'makelore.data-service', packageId: null, title: '开发数据服务', summary: '为当前项目提供 JSON 数据。', category: '系统', tags: ['data'], publisher: 'MakeLore', version: '1.0.0', delivery: 'system_included', projectState: 'enabled', localEnabled: null, assignedAgentIds: ['agent-a'], assignedAgentNames: ['小明'], billing: 'included', updateAvailable: false, unavailable: false, stale: false, commands: [ { kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service' }, { kind: 'open_agent_assignment', projectId: 'project-a', pluginId: 'makelore.data-service' }, { kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service' }, ], official: { catalog: null, library: null, installation: null, project: projectPlugin, detail: { pluginId: 'makelore.data-service', title: '开发数据服务', summary: '为当前项目提供 JSON 数据。', category: '系统', tags: ['data'], providerDisplayName: 'MakeLore', runtimeKind: 'bundled_typed', runtimeStatus: 'enabled', acquisition: 'system_included', usageBilling: 'included', includedOperationCount: 1, meteredOperationCount: 0, descriptionMarkdown: '详细的数据服务介绍。', permissions: ['读取项目名称'], operations: [], stableVersion: '1.0.0', betaVersion: null, stableRelease: { releaseId: 'data-1', version: '1.0.0' }, betaRelease: null, etag: 'data-1', pricingVersionId: null, stale: false, fetchedAt: 1, }, }, local: null, }; const localItem: PluginWorkspaceItem = { key: 'local:pi-web-search', source: 'local', pluginId: 'pi-web-search', packageId: 'pi-web-search', title: 'Pi Web Search', summary: 'npm:pi-web-search', category: null, tags: ['search'], publisher: null, version: '1.2.3', delivery: 'local_installed', projectState: 'not_applicable', localEnabled: true, assignedAgentIds: [], assignedAgentNames: [], billing: 'none', updateAvailable: false, unavailable: false, stale: false, commands: [ { kind: 'disable_local', packageId: 'pi-web-search' }, { kind: 'remove_local', packageId: 'pi-web-search' }, ], official: null, local: { schemaVersion: 1, packageId: 'pi-web-search', displayName: 'Pi Web Search', resolvedVersion: '1.2.3', source: { kind: 'npm', requested: 'npm:pi-web-search', resolved: 'npm:pi-web-search@1.2.3' }, kind: 'mixed', skillEntries: [{ id: 'search', entryPath: 'skills/search/SKILL.md' }], extensionEntries: ['extensions/search.ts'], enabled: true, confirmedExecutableCode: true, installedAt: '2026-09-03T00:00:00Z', }, }; function projection(selected: PluginWorkspaceItem | null = null): PluginWorkspaceProjection { return { items: [officialItem, localItem], selected, counts: { all: 2, available: 0, mine: 2, enabled: 2, update: 0, unavailable: 0 }, notices: [], }; } function props(selected: PluginWorkspaceItem | null = null) { return { projection: projection(selected), filters: { ...filters, selectedKey: selected?.key ?? null }, activeProjectId: 'project-a', activeProjectName: 'Project A', sourceErrors: [], sourceLoading: [], dataService: null, dataServicePending: {}, onFiltersChange: vi.fn(), onRefresh: vi.fn(), onSelect: vi.fn(), onCommand: vi.fn(), isCommandPending: vi.fn().mockReturnValue(false), onConfigureDataService: vi.fn(), onResetDataService: vi.fn(), onRemoveDataServiceCollection: vi.fn(), onRemoveDataServiceProject: vi.fn(), }; } describe('PluginsView', () => { it('renders the unified workspace and exposes URL-backed filter changes', () => { const view = props(); view.sourceErrors = [ { source: 'catalog', message: 'catalog offline' }, { source: 'project', message: 'project unavailable' }, ]; render(); expect(screen.getByRole('heading', { name: '插件' })).toBeVisible(); expect(screen.getByText(/MakeLore 运营发布官方插件/)).toBeVisible(); expect(screen.getByText(/获取、项目启用与伙伴分配彼此独立/)).toBeVisible(); expect(screen.getByText('开发数据服务')).toBeVisible(); expect(screen.getByText('Pi Web Search')).toBeVisible(); expect(screen.getByRole('heading', { name: '本机全局生效' })).toBeVisible(); expect(screen.getByText('本机全局已启用')).toBeVisible(); expect(screen.getByText('随 MakeLore 提供')).toBeVisible(); expect(screen.getByText('已分配 1 位伙伴')).toBeVisible(); expect(screen.getAllByRole('alert')).toHaveLength(2); expect(screen.queryByRole('textbox', { name: /包|路径|source/i })).not.toBeInTheDocument(); expect(screen.getByText(/通过对话安装/)).toBeVisible(); fireEvent.change(screen.getByLabelText('范围'), { target: { value: 'all' } }); expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ scope: 'all' })); fireEvent.change(screen.getByLabelText('来源'), { target: { value: 'local' } }); expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ source: 'local' })); fireEvent.change(screen.getByLabelText('状态'), { target: { value: 'enabled' } }); expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ state: 'enabled' })); fireEvent.change(screen.getByRole('searchbox', { name: '搜索插件' }), { target: { value: 'web' } }); fireEvent.submit(screen.getByRole('search')); expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ search: 'web' })); }); it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => { const view = props(officialItem); render(); const dialog = screen.getByRole('dialog', { name: '开发数据服务' }); expect(dialog).toHaveTextContent('详细的数据服务介绍。'); expect(dialog).toHaveTextContent('读取项目名称'); expect(dialog).toHaveTextContent('data-service.documents'); expect(dialog).toHaveTextContent('按平台包含'); expect(dialog).toHaveTextContent('小明'); expect(dialog).toHaveTextContent('插件专属设置'); expect(dialog).toHaveTextContent('创建开发数据空间'); fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' })); expect(view.onCommand).not.toHaveBeenCalled(); expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A'); fireEvent.click(screen.getByRole('button', { name: '确认从当前项目禁用' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.data-service', }); }); it('dispatches non-destructive detail commands directly and closes from the dialog control', () => { const view = props(officialItem); render(); fireEvent.click(screen.getByRole('button', { name: '分配开发数据服务给伙伴' })); expect(view.onCommand).toHaveBeenCalledWith({ kind: 'open_agent_assignment', projectId: 'project-a', pluginId: 'makelore.data-service', }); fireEvent.click(screen.getByRole('button', { name: '关闭插件详情' })); expect(view.onSelect).toHaveBeenCalledWith(null); }); it('clears a destructive confirmation when the active project changes', () => { const viewA = props(officialItem); const { rerender } = render(); fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' })); expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A'); const projectBItem: PluginWorkspaceItem = { ...officialItem, commands: [ { kind: 'disable_project', projectId: 'project-b', pluginId: 'makelore.data-service' }, ], }; const viewB = { ...props(projectBItem), activeProjectId: 'project-b', activeProjectName: 'Project B', }; rerender(); expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); expect(viewA.onCommand).not.toHaveBeenCalled(); expect(viewB.onCommand).not.toHaveBeenCalled(); }); it('closes the detail with Escape and returns focus to the originating card', async () => { function Harness() { const [selected, setSelected] = useState(null); const view = props(selected); view.onSelect = vi.fn((key: PluginWorkspaceItem['key'] | null) => { setSelected(key === officialItem.key ? officialItem : null); }); return ; } render(); const trigger = screen.getByRole('button', { name: '查看开发数据服务详情' }); fireEvent.click(trigger); const dialog = screen.getByRole('dialog', { name: '开发数据服务' }); expect(dialog).toBeVisible(); await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true)); fireEvent.keyDown(dialog, { key: 'Escape' }); await waitFor(() => expect(screen.queryByRole('dialog', { name: '开发数据服务' })).not.toBeInTheDocument()); await waitFor(() => expect(trigger).toHaveFocus()); }); });