288 lines
11 KiB
TypeScript
288 lines
11 KiB
TypeScript
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(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
|
|
|
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(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
|
|
|
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(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
|
|
|
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(<MemoryRouter><PluginsView {...viewA} /></MemoryRouter>);
|
|
|
|
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(<MemoryRouter><PluginsView {...viewB} /></MemoryRouter>);
|
|
|
|
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<PluginWorkspaceItem | null>(null);
|
|
const view = props(selected);
|
|
view.onSelect = vi.fn((key: PluginWorkspaceItem['key'] | null) => {
|
|
setSelected(key === officialItem.key ? officialItem : null);
|
|
});
|
|
return <PluginsView {...view} />;
|
|
}
|
|
render(<MemoryRouter><Harness /></MemoryRouter>);
|
|
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());
|
|
});
|
|
});
|