Files
makelore/tests/e2e/plugin-marketplace.spec.ts

249 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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('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 };
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string }) => {
const requestPath = request.path ?? '';
requests.push(requestPath);
const item = {
pluginId: 'makelore.notes', title: '灵感笔记', summary: '整理项目灵感。', category: '效率', tags: ['笔记'],
providerDisplayName: 'MakeLore', runtimeKind: 'skill_only', runtimeStatus: 'enabled', acquisition: 'free',
usageBilling: 'included', includedOperationCount: 1, meteredOperationCount: 0, stableVersion: '1.0.0', betaVersion: null,
};
if (requestPath.startsWith('/api/coding/plugin-marketplace/catalog')) {
return { ok: true, data: { status: 200, ok: true, json: {
items: [item], nextCursor: null, total: 1, catalogGeneration: 1,
etag: '"plugins-1-tp-none"', pricingVersionId: null, stale: false, fetchedAt: 1,
} } };
}
if (requestPath === '/api/coding/plugin-marketplace/plugins/makelore.notes') {
return { ok: true, data: { status: 200, ok: true, json: {
...item, descriptionMarkdown: '详细介绍', permissions: ['读取项目名称'], operations: [],
stableRelease: { releaseId: 'release-1', pluginId: 'makelore.notes', version: '1.0.0' }, betaRelease: null,
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('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-discover')).toBeVisible();
await expect(page.getByRole('heading', { name: '灵感笔记' })).toBeVisible();
await page.getByRole('searchbox', { name: '搜索插件' }).fill('笔记');
await page.getByRole('button', { name: '搜索', exact: true }).click();
await page.getByRole('button', { name: '查看详情' }).click();
await expect(page.getByRole('heading', { name: '灵感笔记' }).last()).toBeVisible();
await expect(page.getByText('详细介绍')).toBeVisible();
const requests = await app.evaluate(() => (
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E?.requests ?? []
));
expect(requests).toContain('/api/coding/plugin-marketplace/catalog?limit=24');
expect(requests).toContain('/api/coding/plugin-marketplace/catalog?query=%E7%AC%94%E8%AE%B0&limit=24');
expect(requests).toContain('/api/coding/plugin-marketplace/plugins/makelore.notes');
const marketplaceRequests = requests.filter((requestPath) => requestPath.startsWith('/api/coding/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 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[] = [];
const library = {
items: [
{
pluginId: 'makelore.notes', title: '灵感笔记', summary: '整理项目灵感。', category: '效率',
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled',
acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: '2.0.0-beta.1',
},
{
pluginId: 'makelore.paused', title: '暂停插件', summary: '暂时暂停。', category: '效率',
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'suspended',
acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null,
},
{
pluginId: 'makelore.unavailable', title: '不可用插件', summary: '服务不可用。', category: '效率',
acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled',
acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '1.0.0', betaVersion: null,
},
],
total: 3,
stale: false,
fetchedAt: 1,
};
const projection = () => ({
library,
installations: [
...(installedChannel ? [{
status: 'installed', pluginId: 'makelore.notes', releaseId: installedChannel === 'beta' ? 'beta-2' : 'stable-1',
version: installedChannel === 'beta' ? '2.0.0-beta.1' : installedVersion, channel: installedChannel,
}] : []),
{ status: 'unavailable', pluginId: 'makelore.unavailable', reason: 'plugin_backend_unavailable' },
],
});
const result = (json: unknown) => ({ ok: true, data: { status: 200, ok: true, json } });
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E = { requests };
ipcMain.removeHandler('hostapi:fetch');
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string }) => {
const requestPath = request.path ?? '';
const method = (request.method ?? 'GET').toUpperCase();
requests.push(`${method} ${requestPath}`);
if (requestPath === '/api/auth/session/sync') {
const response = result({
success: true,
session: {
accessToken: 'marketplace-e2e-token', tokenType: 'Bearer',
expiresAt: Date.now() + 3_600_000, lastActiveAt: Date.now(), canRefresh: false,
},
});
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';
return result({ status: 'installed', pluginId: 'makelore.notes', releaseId: 'beta-2', version: '2.0.0-beta.1', channel: 'beta' });
}
if (requestPath === '/api/coding/plugin-marketplace/install/makelore.notes' && method === 'DELETE') {
installedChannel = null;
return result({ status: 'removed', pluginId: 'makelore.notes', releaseId: 'beta-2', version: '2.0.0-beta.1', channel: 'beta' });
}
return { ok: false, error: { message: `Unexpected Host API request: ${method} ${requestPath}` } };
});
}, cachedProjectResponses);
await page.addInitScript(({ key, value }) => {
localStorage.setItem(key, value);
}, {
key: 'niancode-auth',
value: JSON.stringify({
state: {
authBase: 'https://biz.nianxx.cn/auth/', clientId: 'app', accessToken: 'marketplace-e2e-token',
tokenType: 'Bearer', expiresAt: Date.now() + 3_600_000, lastActiveAt: Date.now(), canRefresh: false,
legacyRefreshToken: null,
user: { username: 'marketplace-e2e', userId: 'marketplace-e2e-user', tenantId: null, deptId: null, authorities: [] },
moduleAccess: { programming: true, design: true, learning: true, robot: true },
},
version: 2,
}),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('main-layout')).toBeVisible();
await page.keyboard.press('Escape');
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 ?? [])
.filter((requestPath) => requestPath.includes('/api/auth') || requestPath.includes('/plugin-marketplace'))
))).toEqual(expect.arrayContaining([
'GET /api/auth/me',
'GET /api/coding/plugin-marketplace/library',
]));
await expect(page.getByRole('heading', { name: '暂停插件' })).toBeVisible();
await expect(page.getByText('运行已暂停')).toBeVisible();
await expect(page.getByRole('button', { name: '下载暂停插件' })).toBeDisabled();
await expect(page.getByText('Marketplace 服务暂不可用,设备版本未替换。')).toBeVisible();
await page.getByRole('button', { name: '安装 Beta灵感笔记' }).click();
await expect(page.getByText('当前频道Beta')).toBeVisible();
await expect(page.getByRole('button', { name: '安装 Beta灵感笔记' })).toHaveCount(0);
await page.getByRole('button', { name: '删除设备上的灵感笔记' }).click();
const notesCard = page.locator('article').filter({ has: page.getByRole('heading', { name: '灵感笔记' }) });
await expect(notesCard.getByText('尚未下载到设备')).toBeVisible();
await expect(notesCard.getByRole('button', { name: '下载灵感笔记' })).toBeVisible();
const requests = await app.evaluate(() => (
(globalThis as typeof globalThis & { __marketplaceE2E?: { requests: string[] } }).__marketplaceE2E?.requests ?? []
));
expect(requests).toContain('POST /api/coding/plugin-marketplace/install/makelore.notes/beta');
expect(requests).toContain('DELETE /api/coding/plugin-marketplace/install/makelore.notes');
} finally {
await closeElectronApp(app);
await rm(projectPath, { recursive: true, force: true });
}
});
});