feat(project-config): simplify plugin and model drawers
This commit is contained in:
@@ -9,15 +9,10 @@ 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();
|
||||
@@ -181,15 +176,12 @@ function projection(selected: PluginWorkspaceItem | null = null): PluginWorkspac
|
||||
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),
|
||||
@@ -248,15 +240,21 @@ function preparePluginsRouteState() {
|
||||
}
|
||||
|
||||
describe('PluginsView', () => {
|
||||
it('renders as project-configuration drawer content without a duplicate standalone header', () => {
|
||||
it('renders a direct Skill-like list in the project drawer without filters or refresh', () => {
|
||||
render(<MemoryRouter><PluginsView {...props()} embedded /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByTestId('plugins-page')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '刷新全部来源' })).toBeVisible();
|
||||
expect(screen.queryByRole('heading', { name: '插件', level: 1 })).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('plugin-list')).toHaveClass('space-y-3');
|
||||
expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Pi Web Search:已安装' })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('keeps the production URL, scope control, and fallback notice aligned across in-place changes', async () => {
|
||||
it('keeps the production URL and fallback notice aligned without exposing query controls', async () => {
|
||||
preparePluginsRouteState();
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/project-config/plugins?scope=project']}>
|
||||
@@ -267,7 +265,7 @@ describe('PluginsView', () => {
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
|
||||
const activeProject = {
|
||||
@@ -282,23 +280,23 @@ describe('PluginsView', () => {
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开非法插件地址' }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=project&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('project');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
|
||||
act(() => codingWorkspaceStore.setState({ activeProjectId: null, activeProject: null }));
|
||||
await waitFor(() => expect(screen.getByTestId('plugins-location')).toHaveTextContent(
|
||||
'/project-config/plugins?scope=all&source=all&state=all',
|
||||
));
|
||||
expect(screen.getByLabelText('范围')).toHaveValue('all');
|
||||
expect(screen.queryByLabelText('范围')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps old device and project rows visible and labels each affected snapshot cached after refresh errors', async () => {
|
||||
it('keeps available rows visible when individual sources fail to load', async () => {
|
||||
const activeProject = {
|
||||
id: 'project-a',
|
||||
name: 'Project A',
|
||||
@@ -349,10 +347,10 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter initialEntries={['/project-config/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
|
||||
|
||||
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
|
||||
const heading = await screen.findByRole('heading', { name });
|
||||
expect(heading.closest('article')).toHaveTextContent('缓存');
|
||||
expect(await screen.findByRole('heading', { name })).toBeVisible();
|
||||
}
|
||||
expect(screen.getAllByRole('alert')).toHaveLength(2);
|
||||
expect(screen.queryByText('缓存')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('attempts a rejected official detail load once per open and retries only after close and reopen', async () => {
|
||||
@@ -416,7 +414,7 @@ describe('PluginsView', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(loadDetail).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByRole('alert', { hidden: true })).toHaveTextContent('插件详情刷新失败。 detail offline');
|
||||
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());
|
||||
@@ -424,7 +422,7 @@ describe('PluginsView', () => {
|
||||
await waitFor(() => expect(loadDetail).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it('renders the unified workspace and exposes URL-backed filter changes', () => {
|
||||
it('renders the capability-first list with details and contextual actions', () => {
|
||||
const view = props();
|
||||
view.sourceErrors = [
|
||||
{ source: 'catalog', message: 'catalog offline' },
|
||||
@@ -433,27 +431,15 @@ describe('PluginsView', () => {
|
||||
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('开发数据服务')).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.getByRole('button', { name: '查看开发数据服务详情' })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '开发数据服务:已添加到项目' })).toBeDisabled();
|
||||
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' }));
|
||||
expect(screen.queryByRole('button', { name: /刷新/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('labels an unacquired code-owned bundled plugin as supplied by MakeLore instead of not downloaded', () => {
|
||||
@@ -491,12 +477,11 @@ describe('PluginsView', () => {
|
||||
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
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();
|
||||
expect(screen.getByRole('button', { name: 'MakeLore 项目脚手架:随应用提供', hidden: true })).toBeDisabled();
|
||||
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目脚手架' });
|
||||
expect(dialog).toHaveTextContent('此插件随应用提供,添加到项目后即可使用。');
|
||||
expect(dialog).not.toHaveTextContent('未下载官方包');
|
||||
expect(dialog).not.toHaveTextContent('生效范围');
|
||||
});
|
||||
|
||||
it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => {
|
||||
@@ -505,26 +490,25 @@ describe('PluginsView', () => {
|
||||
|
||||
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('写入文档');
|
||||
expect(dialog).toHaveTextContent('写入项目数据');
|
||||
expect(dialog).not.toHaveTextContent('读取项目名称');
|
||||
expect(dialog).not.toHaveTextContent('data-service.documents');
|
||||
expect(dialog).not.toHaveTextContent('按平台包含');
|
||||
expect(dialog).not.toHaveTextContent('版本 1.0.0');
|
||||
expect(dialog).toHaveTextContent('使用设置');
|
||||
expect(dialog).toHaveTextContent('创建开发数据空间');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' }));
|
||||
expect(view.onCommand).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认从当前项目禁用' }));
|
||||
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', () => {
|
||||
it('describes local plugins by capability without exposing implementation details', () => {
|
||||
const scriptSkill: PluginWorkspaceItem = {
|
||||
...localItem,
|
||||
key: 'local:makelore-project-scaffold',
|
||||
@@ -547,11 +531,13 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(scriptSkill)} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'MakeLore 项目初始化' });
|
||||
expect(dialog).toHaveTextContent('包含可执行 Skill 脚本');
|
||||
expect(dialog).not.toHaveTextContent('包含可执行 Pi extension');
|
||||
expect(dialog).toHaveTextContent('为智能体提供可在本机使用的扩展能力。');
|
||||
expect(dialog).not.toHaveTextContent('Skill 脚本');
|
||||
expect(dialog).not.toHaveTextContent('Pi extension');
|
||||
expect(dialog).not.toHaveTextContent('完整桌面权限');
|
||||
});
|
||||
|
||||
it('shows cached, suspended, retired, and retained-device reason copy with an explicit sign-in action', () => {
|
||||
it('shows a friendly unavailable message and keeps the applicable sign-in action', () => {
|
||||
const unavailableItem: PluginWorkspaceItem = {
|
||||
...officialItem,
|
||||
title: 'Notes',
|
||||
@@ -580,19 +566,21 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
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(dialog).toHaveTextContent('这项能力目前暂不可用');
|
||||
expect(dialog).not.toHaveTextContent('缓存');
|
||||
expect(dialog).not.toHaveTextContent('已暂停');
|
||||
expect(dialog).not.toHaveTextContent('已退役');
|
||||
expect(dialog).not.toHaveTextContent('1.0.0');
|
||||
expect(dialog).not.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', () => {
|
||||
it('keeps Marketplace details capability-focused without exposing operation or pricing fields', () => {
|
||||
const detailItem: PluginWorkspaceItem = {
|
||||
...officialItem,
|
||||
title: 'Notes',
|
||||
summary: '记录、整理并回顾项目笔记。',
|
||||
projectState: 'not_applicable',
|
||||
assignedAgentIds: [],
|
||||
assignedAgentNames: [],
|
||||
@@ -632,54 +620,60 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...view} /></MemoryRouter>);
|
||||
|
||||
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');
|
||||
expect(dialog).toHaveTextContent('记录、整理并回顾项目笔记。');
|
||||
expect(dialog).not.toHaveTextContent('notes');
|
||||
expect(dialog).not.toHaveTextContent('write');
|
||||
expect(dialog).not.toHaveTextContent('每次由服务端结算');
|
||||
expect(dialog).not.toHaveTextContent('request');
|
||||
expect(dialog).not.toHaveTextContent('Token Point');
|
||||
expect(dialog).not.toHaveTextContent('pricing-7');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['stale', '当前项目策略使用缓存,所示能力与价格可能不是最新状态。'],
|
||||
['unavailable', '当前项目策略不可用,无法确认最新能力与价格。'],
|
||||
] as const)('shows %s project policy status beside policy-owned capability and pricing', (projectPolicyStatus, copy) => {
|
||||
['stale'],
|
||||
['unavailable'],
|
||||
] as const)('does not expose the %s policy implementation state in details', (projectPolicyStatus) => {
|
||||
const policyItem: PluginWorkspaceItem = { ...officialItem, projectPolicyStatus };
|
||||
render(<MemoryRouter><PluginsView {...props(policyItem)} /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '开发数据服务' })).toHaveTextContent(copy);
|
||||
});
|
||||
|
||||
it('does not mark current project policy as stale or unavailable', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(officialItem)} /></MemoryRouter>);
|
||||
|
||||
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', () => {
|
||||
it('deduplicates human-facing capability descriptions and hides their technical identity', () => {
|
||||
const capabilityTool = {
|
||||
name: 'notes_write',
|
||||
label: '整理笔记',
|
||||
description: '整理并保存项目笔记。',
|
||||
mutation: 'write' as const,
|
||||
permissions: ['project.notes.write'],
|
||||
};
|
||||
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,
|
||||
id: 'notes.internal',
|
||||
operations: [
|
||||
{
|
||||
id: 'write_internal',
|
||||
billing: {
|
||||
mode: 'platform_metered' as const,
|
||||
availability: 'available' as const,
|
||||
notice: '当前项目策略价格',
|
||||
ratePoints: '7',
|
||||
},
|
||||
tool: capabilityTool,
|
||||
},
|
||||
tool: null,
|
||||
}],
|
||||
{
|
||||
id: 'write_duplicate',
|
||||
billing: {
|
||||
mode: 'included' as const,
|
||||
availability: 'available' as const,
|
||||
notice: '目录包含读取',
|
||||
},
|
||||
tool: capabilityTool,
|
||||
},
|
||||
],
|
||||
}],
|
||||
};
|
||||
const detailItem: PluginWorkspaceItem = {
|
||||
@@ -724,20 +718,21 @@ describe('PluginsView', () => {
|
||||
render(<MemoryRouter><PluginsView {...props(detailItem)} /></MemoryRouter>);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: 'Notes' });
|
||||
expect(screen.getAllByText('write')).toHaveLength(1);
|
||||
expect(dialog).toHaveTextContent('read');
|
||||
expect(dialog).toHaveTextContent('当前项目策略价格');
|
||||
expect(screen.getAllByText('整理笔记')).toHaveLength(1);
|
||||
expect(screen.getAllByText('整理并保存项目笔记。')).toHaveLength(1);
|
||||
expect(dialog).not.toHaveTextContent('notes.internal');
|
||||
expect(dialog).not.toHaveTextContent('write_internal');
|
||||
expect(dialog).not.toHaveTextContent('project.notes.write');
|
||||
expect(dialog).not.toHaveTextContent('当前项目策略价格');
|
||||
expect(dialog).not.toHaveTextContent('目录价格');
|
||||
expect(dialog).toHaveTextContent('每单位 Token Point:7');
|
||||
expect(dialog).toHaveTextContent('价格版本:9');
|
||||
expect(dialog).toHaveTextContent('目录包含读取');
|
||||
expect(dialog).not.toHaveTextContent('Token Point');
|
||||
});
|
||||
|
||||
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: '查看开发数据服务插件设置' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开开发数据服务设置' }));
|
||||
expect(view.onCommand).toHaveBeenCalledWith({
|
||||
kind: 'open_settings', projectId: 'project-a', pluginId: 'makelore.data-service',
|
||||
});
|
||||
@@ -749,7 +744,7 @@ describe('PluginsView', () => {
|
||||
const viewA = props(officialItem);
|
||||
const { rerender } = render(<MemoryRouter><PluginsView {...viewA} /></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '从当前项目禁用开发数据服务' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '从项目移除开发数据服务' }));
|
||||
expect(screen.getByRole('alertdialog')).toHaveTextContent('Project A');
|
||||
|
||||
const projectBItem: PluginWorkspaceItem = {
|
||||
|
||||
Reference in New Issue
Block a user