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

182 lines
11 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 { 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 }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await page.getByTestId('ai-module-option-programming').click();
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,
} } };
}
return { ok: false, error: { message: `Unexpected Host API request: ${requestPath}` } };
});
});
await page.getByTestId('sidebar-nav-plugin-marketplace').click();
await expect(page.getByTestId('plugin-marketplace-page')).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);
}
});
test('keeps Beta explicit and exposes bounded unavailable, disabled, and device-delete states', async ({ launchElectronApp }) => {
const app = await launchElectronApp({ skipSetup: true });
try {
const page = await getStableWindow(app);
await app.evaluate(({ ipcMain }) => {
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/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}` } };
});
});
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('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-my-plugins').click();
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);
}
});
});