import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { useState } from 'react';
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Plugins, PluginsView } from '@/pages/Plugins';
import { useAuthStore } from '@/stores/auth';
import { codingPluginsStore } from '@/stores/coding-plugins';
import { codingWorkspaceStore } from '@/stores/coding-workspace';
import { devicePackageStore } from '@/stores/device-packages';
import { pluginMarketplaceStore } from '@/stores/plugin-marketplace';
import type {
PluginWorkspaceFilters,
PluginWorkspaceItem,
PluginWorkspaceProjection,
} from '@/pages/Plugins/plugin-workspace-model';
const filters: PluginWorkspaceFilters = {
scope: 'project', source: 'all', state: 'all', search: '', selectedKey: null,
};
const initialAuthState = useAuthStore.getInitialState();
const initialCodingPluginsState = codingPluginsStore.getInitialState();
const initialCodingWorkspaceState = codingWorkspaceStore.getInitialState();
const initialDevicePackageState = devicePackageStore.getInitialState();
const initialMarketplaceState = pluginMarketplaceStore.getInitialState();
afterEach(() => {
vi.restoreAllMocks();
act(() => {
useAuthStore.setState(initialAuthState, true);
codingPluginsStore.setState(initialCodingPluginsState, true);
codingWorkspaceStore.setState(initialCodingWorkspaceState, true);
devicePackageStore.setState(initialDevicePackageState, true);
pluginMarketplaceStore.setState(initialMarketplaceState, true);
});
});
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',
projectPolicyStatus: 'current',
localEnabled: null,
assignedAgentIds: ['agent-a'],
assignedAgentNames: ['小明'],
billing: 'included',
updateAvailable: false,
unavailable: false,
stale: false,
suspended: false,
retired: false,
deviceReason: null,
commands: [
{ kind: 'disable_project', 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',
projectPolicyStatus: null,
localEnabled: true,
assignedAgentIds: [],
assignedAgentNames: [],
billing: 'none',
updateAvailable: false,
unavailable: false,
stale: false,
suspended: false,
retired: false,
deviceReason: null,
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(),
};
}
function PluginsRouteHarness() {
const location = useLocation();
const navigate = useNavigate();
return (
<>
{location.pathname}{location.search}
>
);
}
function preparePluginsRouteState() {
useAuthStore.setState({ user: null });
codingWorkspaceStore.setState({
activeProjectId: null,
activeProject: null,
config: null,
loadState: 'ready',
});
codingPluginsStore.setState({ projection: null, loadState: 'ready', error: null });
devicePackageStore.setState({
index: { schemaVersion: 1, generation: 1, packages: [] },
state: 'ready',
error: null,
});
pluginMarketplaceStore.setState({
accountKey: null,
catalog: null,
catalogState: 'ready',
catalogError: null,
library: null,
libraryState: 'idle',
libraryError: null,
installations: {},
details: {},
detailState: {},
});
vi.spyOn(pluginMarketplaceStore.getState(), 'loadCatalog').mockResolvedValue(undefined);
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
}
describe('PluginsView', () => {
it('keeps the production URL, scope control, and fallback notice aligned across in-place changes', async () => {
preparePluginsRouteState();
render(
,
);
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/plugins?scope=all&source=all&state=all',
));
expect(screen.getByLabelText('范围')).toHaveValue('all');
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
const activeProject = {
id: 'project-a',
name: 'Project A',
createdAt: '2026-09-03T00:00:00Z',
updatedAt: '2026-09-03T00:00:00Z',
lastOpenedAt: '2026-09-03T00:00:00Z',
};
act(() => codingWorkspaceStore.setState({ activeProjectId: activeProject.id, activeProject }));
fireEvent.click(screen.getByRole('button', { name: '打开裸插件地址' }));
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/plugins?scope=project&source=all&state=all',
));
expect(screen.getByLabelText('范围')).toHaveValue('project');
fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' }));
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/plugins?scope=project&source=all&state=all',
));
expect(screen.getByLabelText('范围')).toHaveValue('project');
act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null }));
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
'/plugins?scope=all&source=all&state=all',
));
expect(screen.getByLabelText('范围')).toHaveValue('all');
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
});
it('keeps old device and project rows visible and labels each affected snapshot cached after refresh errors', async () => {
const activeProject = {
id: 'project-a',
name: 'Project A',
createdAt: '2026-09-03T00:00:00Z',
updatedAt: '2026-09-03T00:00:00Z',
lastOpenedAt: '2026-09-03T00:00:00Z',
};
useAuthStore.setState({ user: null });
codingWorkspaceStore.setState({
activeProjectId: activeProject.id,
activeProject,
config: null,
loadState: 'ready',
});
codingPluginsStore.setState({
projectId: activeProject.id,
projection: {
schemaVersion: 1,
project: { localProjectId: activeProject.id, durableProjectId: 'durable-a' },
policyStatus: 'current',
unknownPluginIds: ['makelore.retained'],
items: [projectPlugin],
},
loadState: 'error',
error: 'project refresh failed',
});
devicePackageStore.setState({
index: { schemaVersion: 1, generation: 8, packages: [localItem.local!] },
state: 'error',
error: 'device refresh failed',
});
pluginMarketplaceStore.setState({
accountKey: null,
catalog: null,
catalogState: 'ready',
catalogError: null,
library: null,
libraryState: 'idle',
libraryError: null,
installations: {},
details: {},
detailState: {},
});
vi.spyOn(pluginMarketplaceStore.getState(), 'loadCatalog').mockResolvedValue(undefined);
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
render();
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
const heading = await screen.findByRole('heading', { name });
expect(heading.closest('article')).toHaveTextContent('缓存');
}
expect(screen.getAllByRole('alert')).toHaveLength(2);
});
it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => {
preparePluginsRouteState();
pluginMarketplaceStore.setState({
catalog: {
items: [{
pluginId: 'makelore.notes',
title: 'Notes',
summary: 'Write project notes',
category: 'productivity',
tags: ['notes'],
providerDisplayName: 'MakeLore',
runtimeKind: 'skill_only',
runtimeStatus: 'enabled',
acquisition: 'free',
usageBilling: 'token_point',
includedOperationCount: 0,
meteredOperationCount: 1,
stableVersion: '2.0.0',
betaVersion: null,
}],
nextCursor: null,
total: 1,
catalogGeneration: 1,
etag: 'catalog-1',
pricingVersionId: 'pricing-1',
stale: false,
fetchedAt: 1,
},
});
let rejectFirstAttempt: (reason?: unknown) => void = () => undefined;
const firstAttempt = new Promise((_resolve, reject) => {
rejectFirstAttempt = reject;
});
const loadDetail = vi.spyOn(pluginMarketplaceStore.getState(), 'loadDetail')
.mockImplementationOnce(async () => await firstAttempt)
.mockResolvedValue(undefined);
render(
,
);
await waitFor(() => expect(loadDetail).toHaveBeenCalledTimes(1));
await act(async () => {
pluginMarketplaceStore.setState((state) => ({
detailState: { ...state.detailState, 'makelore.notes': 'error' },
catalogError: 'detail offline',
}));
rejectFirstAttempt(new Error('detail offline'));
await Promise.resolve();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(loadDetail).toHaveBeenCalledTimes(1);
expect(screen.getByRole('alert', { hidden: true })).toHaveTextContent('插件详情刷新失败。 detail offline');
fireEvent.click(screen.getByRole('button', { name: '关闭插件详情' }));
await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Notes' })).not.toBeInTheDocument());
fireEvent.click(screen.getByRole('button', { name: '查看Notes详情' }));
await waitFor(() => expect(loadDetail).toHaveBeenCalledTimes(2));
});
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('随项目启用')).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('labels an unacquired code-owned bundled plugin as supplied by MakeLore instead of not downloaded', () => {
const scaffoldItem: PluginWorkspaceItem = {
...officialItem,
key: 'official:makelore.project-scaffold',
pluginId: 'makelore.project-scaffold',
title: 'MakeLore 项目脚手架',
delivery: 'not_acquired',
projectState: 'disabled',
assignedAgentIds: [],
assignedAgentNames: [],
commands: [],
official: {
catalog: null,
detail: null,
library: null,
installation: null,
project: {
...projectPlugin,
id: 'makelore.project-scaffold',
displayName: 'MakeLore 项目脚手架',
enabled: false,
state: 'disabled',
},
},
};
const view = props(scaffoldItem);
view.projection = {
items: [scaffoldItem],
selected: scaffoldItem,
counts: { all: 1, available: 0, mine: 0, enabled: 0, update: 0, unavailable: 0 },
notices: [],
};
render();
expect(screen.getAllByText('随应用提供')).toHaveLength(2);
expect(screen.queryByText('未下载官方包')).not.toBeInTheDocument();
expect(screen.getByText('随项目启用')).toBeVisible();
expect(screen.getByRole('heading', { name: '生效范围' })).toBeVisible();
expect(screen.getByText(/启用当前项目后.*无需单独分配/)).toBeVisible();
expect(screen.queryByRole('heading', { name: '伙伴分配' })).not.toBeInTheDocument();
});
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).not.toHaveTextContent('小明');
expect(screen.queryByRole('button', { name: '分配开发数据服务给伙伴' })).not.toBeInTheDocument();
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('describes executable Skill scripts without calling them Pi extensions', () => {
const scriptSkill: PluginWorkspaceItem = {
...localItem,
key: 'local:makelore-project-scaffold',
pluginId: 'makelore-project-scaffold',
packageId: 'makelore-project-scaffold',
title: 'MakeLore 项目初始化',
local: {
...localItem.local!,
packageId: 'makelore-project-scaffold',
displayName: 'MakeLore 项目初始化',
kind: 'skill-only',
skillEntries: [{
id: 'makelore-project-scaffold',
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
}],
extensionEntries: [],
},
};
render();
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' });
expect(dialog).toHaveTextContent('包含可执行 Skill 脚本');
expect(dialog).not.toHaveTextContent('包含可执行 Pi extension');
});
it('shows cached, suspended, retired, and retained-device reason copy with an explicit sign-in action', () => {
const unavailableItem: PluginWorkspaceItem = {
...officialItem,
title: 'Notes',
delivery: 'account_unknown',
projectState: 'not_applicable',
version: '1.0.0',
stale: true,
unavailable: true,
suspended: true,
retired: true,
deviceReason: '当前客户端与新版本不兼容',
commands: [{ kind: 'sign_in' }],
official: {
...officialItem.official!,
installation: {
status: 'unavailable',
pluginId: 'makelore.notes',
version: '1.0.0',
channel: 'stable',
reason: '当前客户端与新版本不兼容',
},
project: null,
},
};
const view = props(unavailableItem);
render();
const dialog = screen.getByRole('dialog', { name: 'Notes' });
expect(dialog).toHaveTextContent('缓存');
expect(dialog).toHaveTextContent('已暂停');
expect(dialog).toHaveTextContent('已退役');
expect(dialog).toHaveTextContent('官方包 1.0.0');
expect(dialog).toHaveTextContent('旧设备版本已保留:当前客户端与新版本不兼容');
fireEvent.click(screen.getByRole('button', { name: '登录后免费获取Notes' }));
expect(view.onCommand).toHaveBeenCalledWith({ kind: 'sign_in' });
});
it('shows Marketplace detail operations and server-owned pricing without an active project', () => {
const detailItem: PluginWorkspaceItem = {
...officialItem,
title: 'Notes',
projectState: 'not_applicable',
assignedAgentIds: [],
assignedAgentNames: [],
official: {
...officialItem.official!,
project: null,
detail: {
...officialItem.official!.detail!,
pluginId: 'makelore.notes',
title: 'Notes',
operations: [{
capabilityId: 'notes',
operation: 'write',
executionMode: 'job',
billing: {
mode: 'platform_metered',
status: null,
notice: '每次由服务端结算',
entitlementScope: 'account',
unitName: 'request',
unitSize: 2,
ratePoints: '3',
minimumChargePoints: '1',
roundingMode: 'ceil',
pricingVersion: 'pricing-7',
},
enabled: true,
}],
},
},
};
const view = {
...props(detailItem),
activeProjectId: null,
activeProjectName: null,
};
render();
const dialog = screen.getByRole('dialog', { name: 'Notes' });
expect(dialog).toHaveTextContent('notes');
expect(dialog).toHaveTextContent('write');
expect(dialog).toHaveTextContent('每次由服务端结算');
expect(dialog).toHaveTextContent('计费单位:request');
expect(dialog).toHaveTextContent('单位大小:2');
expect(dialog).toHaveTextContent('每单位 Token Point:3');
expect(dialog).toHaveTextContent('最低扣点:1');
expect(dialog).toHaveTextContent('价格版本:pricing-7');
});
it.each([
['stale', '当前项目策略使用缓存,所示能力与价格可能不是最新状态。'],
['unavailable', '当前项目策略不可用,无法确认最新能力与价格。'],
] as const)('shows %s project policy status beside policy-owned capability and pricing', (projectPolicyStatus, copy) => {
const policyItem: PluginWorkspaceItem = { ...officialItem, projectPolicyStatus };
render();
expect(screen.getByRole('dialog', { name: '开发数据服务' })).toHaveTextContent(copy);
});
it('does not mark current project policy as stale or unavailable', () => {
render();
const dialog = screen.getByRole('dialog', { name: '开发数据服务' });
expect(dialog).not.toHaveTextContent('当前项目策略使用缓存');
expect(dialog).not.toHaveTextContent('当前项目策略不可用');
});
it('deduplicates operation identity while preferring the current project policy projection', () => {
const projectPolicy = {
...projectPlugin,
capabilities: [{
id: 'notes',
operations: [{
id: 'write',
billing: {
mode: 'platform_metered' as const,
availability: 'available' as const,
notice: '当前项目策略价格',
unitName: 'project-request',
unitSize: 1,
ratePoints: '7',
minimumChargePoints: '2',
pricingVersion: 9,
roundingMode: 'ceil' as const,
},
tool: null,
}],
}],
};
const detailItem: PluginWorkspaceItem = {
...officialItem,
title: 'Notes',
official: {
...officialItem.official!,
project: projectPolicy,
detail: {
...officialItem.official!.detail!,
pluginId: 'makelore.notes',
title: 'Notes',
operations: [
{
capabilityId: 'notes',
operation: 'write',
executionMode: 'synchronous',
billing: {
mode: 'platform_metered', status: null, notice: '目录价格',
entitlementScope: null, unitName: 'catalog-request', unitSize: 1,
ratePoints: '3', minimumChargePoints: null, roundingMode: 'ceil',
pricingVersion: 'pricing-3',
},
enabled: true,
},
{
capabilityId: 'notes',
operation: 'read',
executionMode: 'synchronous',
billing: {
mode: 'included', status: null, notice: '目录包含读取',
entitlementScope: null, unitName: null, unitSize: null,
ratePoints: null, minimumChargePoints: null, roundingMode: null,
pricingVersion: 'pricing-3',
},
enabled: true,
},
],
},
},
};
render();
const dialog = screen.getByRole('dialog', { name: 'Notes' });
expect(screen.getAllByText('write')).toHaveLength(1);
expect(dialog).toHaveTextContent('read');
expect(dialog).toHaveTextContent('当前项目策略价格');
expect(dialog).not.toHaveTextContent('目录价格');
expect(dialog).toHaveTextContent('每单位 Token Point:7');
expect(dialog).toHaveTextContent('价格版本:9');
expect(dialog).toHaveTextContent('目录包含读取');
});
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_settings', 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());
});
});