feat: move plugin center into project configuration
This commit is contained in:
@@ -1,11 +1,23 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Plugin Marketplace', () => {
|
||||
test('opens the global catalog, searches, and reads detail through fixed Main routes', async ({ launchElectronApp }) => {
|
||||
test.describe('Project Plugin Services marketplace tabs', () => {
|
||||
test('opens discovery inside project configuration, searches, and reads detail through fixed Main routes', async ({ launchElectronApp }) => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-marketplace-e2e-'));
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
await app.evaluate(({ ipcMain }, selectedPath) => {
|
||||
ipcMain.removeHandler('dialog:open');
|
||||
ipcMain.handle('dialog:open', async () => ({ canceled: false, filePaths: [selectedPath] }));
|
||||
}, projectPath);
|
||||
const page = await getStableWindow(app);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.getByTestId('sidebar-create-project').click();
|
||||
await page.getByRole('button', { name: '选择路径' }).click();
|
||||
await page.getByRole('button', { name: '确认创建' }).click();
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await app.evaluate(({ ipcMain }) => {
|
||||
const requests: string[] = [];
|
||||
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E = { requests };
|
||||
@@ -31,14 +43,25 @@ test.describe('Plugin Marketplace', () => {
|
||||
etag: '"plugins-1-tp-none"', pricingVersionId: null, stale: false, fetchedAt: 1,
|
||||
} } };
|
||||
}
|
||||
if (requestPath.startsWith('/api/coding/plugins')) {
|
||||
const localProjectId = new URL(requestPath, 'https://makelore.local').searchParams.get('projectId') ?? '';
|
||||
return { ok: true, data: { status: 200, ok: true, json: {
|
||||
schemaVersion: 1,
|
||||
project: { localProjectId, durableProjectId: '11111111-1111-4111-8111-111111111111' },
|
||||
policyStatus: 'current', unknownPluginIds: [], items: [],
|
||||
} } };
|
||||
}
|
||||
return { ok: false, error: { message: `Unexpected Host API request: ${requestPath}` } };
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId('sidebar-nav-plugin-marketplace').click();
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
await page.getByRole('tab', { name: '发现插件' }).click();
|
||||
await expect(page.getByRole('dialog', { name: '插件服务' })).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-nav-plugin-marketplace')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-nav-my-plugins')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-nav-project-plugins')).toHaveCount(0);
|
||||
await expect(page.getByTestId('plugin-marketplace-page')).toBeVisible();
|
||||
await expect(page.getByTestId('plugin-marketplace-discover')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '灵感笔记' })).toBeVisible();
|
||||
await page.getByRole('searchbox', { name: '搜索插件' }).fill('笔记');
|
||||
await page.getByRole('button', { name: '搜索', exact: true }).click();
|
||||
@@ -56,15 +79,39 @@ test.describe('Plugin Marketplace', () => {
|
||||
expect(JSON.stringify(marketplaceRequests)).not.toMatch(/account|admission|release_id|path=/i);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await rm(projectPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps Beta explicit and exposes bounded unavailable, disabled, and device-delete states', async ({ launchElectronApp }) => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-my-plugins-e2e-'));
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
await app.evaluate(({ ipcMain }, selectedPath) => {
|
||||
ipcMain.removeHandler('dialog:open');
|
||||
ipcMain.handle('dialog:open', async () => ({ canceled: false, filePaths: [selectedPath] }));
|
||||
}, projectPath);
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-option-programming')).toBeVisible();
|
||||
await app.evaluate(({ ipcMain }) => {
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await page.getByTestId('sidebar-create-project').click();
|
||||
await page.getByRole('button', { name: '选择路径' }).click();
|
||||
await page.getByRole('button', { name: '确认创建' }).click();
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
const cachedProjectResponses = await page.evaluate(async () => {
|
||||
const list = await window.electron.ipcRenderer.invoke('hostapi:fetch', {
|
||||
path: '/api/coding/projects', method: 'GET',
|
||||
});
|
||||
const activeProjectId = (list as { data?: { json?: { activeProjectId?: string } } }).data?.json?.activeProjectId ?? '';
|
||||
const config = await window.electron.ipcRenderer.invoke('hostapi:fetch', {
|
||||
path: `/api/coding/projects/config?projectId=${encodeURIComponent(activeProjectId)}`, method: 'GET',
|
||||
});
|
||||
const conversations = await window.electron.ipcRenderer.invoke('hostapi:fetch', {
|
||||
path: `/api/coding/projects/conversations?projectId=${encodeURIComponent(activeProjectId)}`, method: 'GET',
|
||||
});
|
||||
return { list, config, conversations };
|
||||
});
|
||||
await app.evaluate(({ ipcMain }, projectResponses: { list: unknown; config: unknown; conversations: unknown }) => {
|
||||
let installedChannel: 'stable' | 'beta' | null = 'stable';
|
||||
const installedVersion = '1.0.0';
|
||||
const requests: string[] = [];
|
||||
@@ -118,10 +165,21 @@ test.describe('Plugin Marketplace', () => {
|
||||
return response;
|
||||
}
|
||||
if (requestPath === '/api/auth/me') return result({ success: true, moduleAccess: { programming: true } });
|
||||
if (requestPath === '/api/coding/projects') return projectResponses.list;
|
||||
if (requestPath.startsWith('/api/coding/projects/config')) return projectResponses.config;
|
||||
if (requestPath.startsWith('/api/coding/projects/conversations')) return projectResponses.conversations;
|
||||
if (requestPath.startsWith('/api/coding/plugin-marketplace/catalog')) return result({
|
||||
items: [], nextCursor: null, total: 0, catalogGeneration: 1,
|
||||
etag: '"plugins-1-tp-none"', pricingVersionId: null, stale: false, fetchedAt: 1,
|
||||
});
|
||||
if (requestPath.startsWith('/api/coding/plugins')) {
|
||||
const localProjectId = new URL(requestPath, 'https://makelore.local').searchParams.get('projectId') ?? '';
|
||||
return result({
|
||||
schemaVersion: 1,
|
||||
project: { localProjectId, durableProjectId: '11111111-1111-4111-8111-111111111111' },
|
||||
policyStatus: 'current', unknownPluginIds: [], items: [],
|
||||
});
|
||||
}
|
||||
if (requestPath === '/api/coding/plugin-marketplace/library') return result(projection());
|
||||
if (requestPath === '/api/coding/plugin-marketplace/install/makelore.notes/beta' && method === 'POST') {
|
||||
installedChannel = 'beta';
|
||||
@@ -133,7 +191,7 @@ test.describe('Plugin Marketplace', () => {
|
||||
}
|
||||
return { ok: false, error: { message: `Unexpected Host API request: ${method} ${requestPath}` } };
|
||||
});
|
||||
});
|
||||
}, cachedProjectResponses);
|
||||
await page.addInitScript(({ key, value }) => {
|
||||
localStorage.setItem(key, value);
|
||||
}, {
|
||||
@@ -150,13 +208,13 @@ test.describe('Plugin Marketplace', () => {
|
||||
}),
|
||||
});
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByTestId('ai-module-option-programming')).toBeVisible();
|
||||
const programmingOption = page.getByTestId('ai-module-option-programming');
|
||||
await programmingOption.click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByTestId('sidebar-nav-plugin-marketplace').click();
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
await page.getByRole('tab', { name: '我的插件' }).click();
|
||||
await expect(page.getByRole('dialog', { name: '插件服务' })).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-nav-plugin-marketplace')).toHaveCount(0);
|
||||
await expect(page.getByTestId('my-plugins-page')).toBeVisible();
|
||||
await expect.poll(async () => app.evaluate(() => (
|
||||
((globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E?.requests ?? [])
|
||||
@@ -184,6 +242,7 @@ test.describe('Plugin Marketplace', () => {
|
||||
expect(requests).toContain('DELETE /api/coding/plugin-marketplace/install/makelore.notes');
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await rm(projectPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Project Plugin Center', () => {
|
||||
test.describe('Project Plugin Services', () => {
|
||||
test('enables selection without configuring Data Service or duplicating project IDs', async ({ launchElectronApp }) => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-plugins-e2e-'));
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
@@ -52,6 +52,11 @@ test.describe('Project Plugin Center', () => {
|
||||
});
|
||||
|
||||
await page.getByTestId('resource-card-plugins').click();
|
||||
await expect(page.getByRole('dialog', { name: '插件服务' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '发现插件' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '我的插件' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '项目启用' })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.getByTestId('sidebar-nav-plugin-marketplace')).toHaveCount(0);
|
||||
await expect(page.getByTestId('project-plugins-page')).toBeVisible();
|
||||
await page.getByTestId('project-plugins-page').getByRole('button', { name: '刷新' }).click();
|
||||
await expect.poll(async () => app.evaluate(() => (
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('Plugin Marketplace pages', () => {
|
||||
onSearch={onSearch} onSelect={vi.fn()} onAcquire={onAcquire}
|
||||
/></MemoryRouter>);
|
||||
|
||||
expect(screen.getByRole('heading', { name: '插件中心' })).toHaveClass('text-balance');
|
||||
expect(screen.getByRole('heading', { name: '发现插件' })).toHaveClass('text-balance');
|
||||
expect(screen.getByText('部分能力包含,部分按 Token Point 用量计费')).toBeVisible();
|
||||
expect(screen.getByText('1')).toHaveClass('tabular-nums');
|
||||
fireEvent.change(screen.getByRole('searchbox', { name: '搜索插件' }), { target: { value: '笔记' } });
|
||||
@@ -94,7 +94,7 @@ describe('My Plugins', () => {
|
||||
expect(onReacquire).toHaveBeenCalledWith('makelore.removed');
|
||||
const projectLinks = screen.getAllByRole('link', { name: '启用到项目' });
|
||||
expect(projectLinks.length).toBeGreaterThan(0);
|
||||
expect(projectLinks[0]).toHaveAttribute('href', '/project-config?resource=plugins');
|
||||
expect(projectLinks[0]).toHaveAttribute('href', '/project-config?resource=plugins&pluginTab=project');
|
||||
expect(screen.getAllByRole('link', { name: '分配给伙伴' }).length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('运行已暂停')).toBeVisible();
|
||||
expect(screen.getByText('已退役,移除后不可重新获取')).toBeVisible();
|
||||
@@ -131,7 +131,7 @@ describe('My Plugins', () => {
|
||||
expect(screen.getByRole('link', { name: '分配给伙伴' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders as the account/device tab inside the unified plugin hub', () => {
|
||||
it('renders as the account/device tab inside project plugin services', () => {
|
||||
render(<MemoryRouter><MyPluginsView
|
||||
embedded library={{ ...library, items: [] }} installations={{}} state="ready"
|
||||
pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
|
||||
@@ -140,6 +140,7 @@ describe('My Plugins', () => {
|
||||
|
||||
expect(screen.getByRole('heading', { name: '我的插件', level: 2 })).toBeVisible();
|
||||
expect(screen.getByTestId('my-plugins-page').tagName).toBe('SECTION');
|
||||
expect(screen.getByRole('link', { name: '浏览插件' })).toHaveAttribute('href', '/project-config?resource=plugins&pluginTab=discover');
|
||||
});
|
||||
|
||||
it('updates an installed Beta package only through the explicit Beta action', () => {
|
||||
|
||||
47
tests/unit/plugin-services.test.tsx
Normal file
47
tests/unit/plugin-services.test.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useState } from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { PluginServices } from '@/components/plugins/PluginServices';
|
||||
import { getPluginServiceTab, type PluginServiceTab } from '@/lib/plugin-services';
|
||||
|
||||
vi.mock('@/pages/PluginMarketplace', () => ({
|
||||
PluginMarketplaceCatalog: () => <div>发现插件内容</div>,
|
||||
}));
|
||||
vi.mock('@/pages/MyPlugins', () => ({
|
||||
MyPlugins: () => <div>我的插件内容</div>,
|
||||
}));
|
||||
vi.mock('@/pages/ProjectPlugins', () => ({
|
||||
ProjectPlugins: () => <div>项目启用内容</div>,
|
||||
}));
|
||||
|
||||
function Harness() {
|
||||
const [value, setValue] = useState<PluginServiceTab>('project');
|
||||
return <PluginServices
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
onProjectPluginsChanged={vi.fn()}
|
||||
onManageAgents={vi.fn()}
|
||||
onResolveIdentity={vi.fn()}
|
||||
/>;
|
||||
}
|
||||
|
||||
describe('PluginServices', () => {
|
||||
it('keeps discovery, account/device management, and project enablement in one surface', () => {
|
||||
render(<MemoryRouter><Harness /></MemoryRouter>);
|
||||
|
||||
expect(screen.getByRole('tab', { name: '项目启用' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByText('项目启用内容')).toBeVisible();
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: '发现插件' }), { button: 0, ctrlKey: false });
|
||||
expect(screen.getByText('发现插件内容')).toBeVisible();
|
||||
fireEvent.mouseDown(screen.getByRole('tab', { name: '我的插件' }), { button: 0, ctrlKey: false });
|
||||
expect(screen.getByText('我的插件内容')).toBeVisible();
|
||||
});
|
||||
|
||||
it('defaults invalid and missing deep-link values to project enablement', () => {
|
||||
expect(getPluginServiceTab(null)).toBe('project');
|
||||
expect(getPluginServiceTab('unknown')).toBe('project');
|
||||
expect(getPluginServiceTab('discover')).toBe('discover');
|
||||
expect(getPluginServiceTab('mine')).toBe('mine');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user