Files
makelore/tests/unit/plugin-marketplace-pages.test.tsx
2026-08-31 16:30:17 +08:00

246 lines
14 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 { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { describe, expect, it, vi } from 'vitest';
import { PluginMarketplaceView } from '@/pages/PluginMarketplace';
import { MyPluginsView } from '@/pages/MyPlugins';
import type {
MarketplaceCatalogPage,
MarketplaceLibrarySnapshot,
MarketplacePluginDetail,
} from '@/lib/plugin-marketplace';
const item = {
pluginId: 'makelore.notes', title: '灵感笔记', summary: '整理项目灵感与任务。',
category: '效率', tags: ['笔记'], providerDisplayName: 'MakeLore',
runtimeKind: 'skill_only' as const, runtimeStatus: 'enabled' as const,
acquisition: 'free' as const, usageBilling: 'mixed' as const,
includedOperationCount: 2, meteredOperationCount: 1,
stableVersion: '2.0.0', betaVersion: null,
};
const catalog: MarketplaceCatalogPage = {
items: [item], nextCursor: null, total: 1, catalogGeneration: 2,
etag: '"plugins-2-tp-1"', pricingVersionId: '1', stale: false, fetchedAt: 1,
};
const detail: MarketplacePluginDetail = {
...item, descriptionMarkdown: '详细介绍', permissions: ['读取项目名称'], operations: [],
stableRelease: { releaseId: 'release-2', version: '2.0.0' }, betaRelease: null,
etag: '"plugins-2-tp-1"', pricingVersionId: '1', stale: false, fetchedAt: 1,
};
describe('Plugin Marketplace pages', () => {
it('supports search/detail and keeps free acquisition separate', () => {
const onSearch = vi.fn();
const onAcquire = vi.fn();
render(<MemoryRouter><PluginMarketplaceView
catalog={catalog} detail={detail} library={null} catalogState="ready" pending={{}}
onSearch={onSearch} onSelect={vi.fn()} onAcquire={onAcquire}
/></MemoryRouter>);
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: '笔记' } });
fireEvent.submit(screen.getByRole('search'));
expect(onSearch).toHaveBeenCalledWith(expect.objectContaining({ query: '笔记' }));
const acquire = screen.getByRole('button', { name: '免费获取灵感笔记' });
expect(acquire).toHaveClass('min-h-10');
fireEvent.click(acquire);
expect(onAcquire).toHaveBeenCalledWith('makelore.notes');
});
it('renders the catalog first-failure and stale states without invented cards', () => {
const { rerender } = render(<MemoryRouter><PluginMarketplaceView
catalog={null} detail={null} library={null} catalogState="error" catalogError="offline"
pending={{}} onSearch={vi.fn()} onSelect={vi.fn()} onAcquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByRole('alert')).toHaveTextContent('插件目录暂不可用');
expect(screen.queryByRole('article')).not.toBeInTheDocument();
rerender(<MemoryRouter><PluginMarketplaceView
catalog={{ ...catalog, stale: true }} detail={null} library={null} catalogState="ready"
pending={{}} onSearch={vi.fn()} onSelect={vi.fn()} onAcquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByRole('status')).toHaveTextContent('缓存目录');
});
});
describe('My Plugins', () => {
const library: MarketplaceLibrarySnapshot = {
total: 4, stale: false, fetchedAt: 1,
items: [
{ pluginId: 'makelore.notes', title: '灵感笔记', summary: 'Notes', category: '效率', acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled', acquiredAt: '2026-08-28T00:00:00Z', removedAt: null, stableVersion: '2.0.0', betaVersion: '2.1.0-beta.1' },
{ pluginId: 'makelore.removed', title: '旧插件', summary: 'Removed', category: '效率', acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled', acquiredAt: '2026-08-27T00:00:00Z', removedAt: '2026-08-28T00:00:00Z', stableVersion: '1.0.0', betaVersion: null },
{ pluginId: 'makelore.retired', title: '已退役插件', summary: 'Retired', category: '效率', acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'retired', runtimeStatus: 'enabled', acquiredAt: '2026-08-26T00:00:00Z', removedAt: '2026-08-28T00:00:00Z', stableVersion: '1.0.0', betaVersion: null },
{ pluginId: 'makelore.system', title: '开发数据服务', summary: 'Data', category: '系统', acquisition: 'system_included', acquisitionMode: 'system_included', catalogStatus: 'active', runtimeStatus: 'suspended', acquiredAt: null, removedAt: null, stableVersion: '1.0.0', betaVersion: null },
],
};
it('shows update/remove/reacquire and project/partner actions without chaining them', () => {
const onUpdate = vi.fn();
const onRemove = vi.fn();
const onReacquire = vi.fn();
render(<MemoryRouter><MyPluginsView
library={library} installations={{ 'makelore.notes': { status: 'installed', pluginId: 'makelore.notes', version: '1.5.0', releaseId: 'release-1' } }}
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={onUpdate}
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={onRemove} onReacquire={onReacquire}
/></MemoryRouter>);
fireEvent.click(screen.getByRole('button', { name: '更新灵感笔记' }));
expect(onUpdate).toHaveBeenCalledWith('makelore.notes');
expect(onRemove).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '移除灵感笔记' }));
expect(onRemove).toHaveBeenCalledWith('makelore.notes');
fireEvent.click(screen.getByRole('button', { name: '重新免费获取旧插件' }));
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(screen.getAllByRole('link', { name: '分配给伙伴' }).length).toBeGreaterThan(0);
expect(screen.getByText('运行已暂停')).toBeVisible();
expect(screen.getByText('已退役,移除后不可重新获取')).toBeVisible();
});
it('offers an explicit Beta action without conflating it with stable updates', () => {
const onInstallBeta = vi.fn();
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [library.items[0]] }}
installations={{ 'makelore.notes': { status: 'installed', pluginId: 'makelore.notes', version: '2.0.0', releaseId: 'stable-2' } }}
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
onInstallBeta={onInstallBeta} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
const beta = screen.getByRole('button', { name: '安装 Beta灵感笔记' });
fireEvent.click(beta);
expect(onInstallBeta).toHaveBeenCalledWith('makelore.notes');
expect(screen.getByText('稳定版 2.0.0')).toBeVisible();
});
it('shows system-included plugins as supplied with MakeLore without device package actions', () => {
const systemIncluded = library.items[3]!;
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [systemIncluded] }} installations={{}} state="ready"
pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByText('随 MakeLore 提供')).toBeVisible();
expect(screen.queryByText('尚未下载到设备')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '下载开发数据服务' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '更新开发数据服务' })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: '删除设备上的开发数据服务' })).not.toBeInTheDocument();
expect(screen.getByRole('link', { name: '启用到项目' })).toBeVisible();
expect(screen.getByRole('link', { name: '分配给伙伴' })).toBeVisible();
});
it('renders as the account/device tab inside the unified plugin hub', () => {
render(<MemoryRouter><MyPluginsView
embedded library={{ ...library, items: [] }} installations={{}} state="ready"
pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByRole('heading', { name: '我的插件', level: 2 })).toBeVisible();
expect(screen.getByTestId('my-plugins-page').tagName).toBe('SECTION');
});
it('updates an installed Beta package only through the explicit Beta action', () => {
const onUpdate = vi.fn();
const onInstallBeta = vi.fn();
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [library.items[0]] }}
installations={{ 'makelore.notes': { status: 'installed', pluginId: 'makelore.notes', channel: 'beta', version: '2.0.0', releaseId: 'beta-1' } }}
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={onUpdate}
onInstallBeta={onInstallBeta} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.queryByRole('button', { name: '更新灵感笔记' })).not.toBeInTheDocument();
const updateBeta = screen.getByRole('button', { name: '更新 Beta灵感笔记' });
fireEvent.click(updateBeta);
expect(onInstallBeta).toHaveBeenCalledWith('makelore.notes');
expect(onUpdate).not.toHaveBeenCalled();
expect(screen.getByText('当前频道Beta')).toBeVisible();
});
it('keeps a failed Beta update on the explicit Beta route', () => {
const onUpdate = vi.fn();
const onInstallBeta = vi.fn();
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [library.items[0]] }}
installations={{
'makelore.notes': {
status: 'unavailable',
pluginId: 'makelore.notes',
channel: 'beta',
version: '2.0.0',
releaseId: 'beta-1',
reason: 'plugin_release_unavailable',
},
}}
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={onUpdate}
onInstallBeta={onInstallBeta} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
fireEvent.click(screen.getByRole('button', { name: '更新 Beta灵感笔记' }));
expect(onInstallBeta).toHaveBeenCalledWith('makelore.notes');
expect(onUpdate).not.toHaveBeenCalled();
expect(screen.getByText('当前频道Beta')).toBeVisible();
});
it('does not silently fall back to stable when the installed Beta channel has no release', () => {
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [{ ...library.items[0], betaVersion: null }] }}
installations={{ 'makelore.notes': { status: 'installed', pluginId: 'makelore.notes', channel: 'beta', version: '2.0.0', releaseId: 'beta-1' } }}
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByText('当前 Beta 频道暂无可用版本;不会静默切回稳定版。')).toBeVisible();
expect(screen.queryByRole('button', { name: '更新灵感笔记' })).not.toBeInTheDocument();
});
it('names signature, yanked/not-ready, and incompatible failures without hiding the old install', () => {
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [library.items[0]] }}
installations={{ 'makelore.notes': { status: 'unavailable', pluginId: 'makelore.notes', version: '1.5.0', reason: 'plugin_signature_invalid plugin_release_yanked plugin_incompatible_client' } }}
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByText(/签名校验失败/)).toBeVisible();
expect(screen.getByText(/此版本已撤回/)).toBeVisible();
expect(screen.getByText(/当前 MakeLore 版本不兼容/)).toBeVisible();
expect(screen.getByText('设备版本 1.5.0')).toBeVisible();
});
it('does not present a no-version unavailable projection as an installed device package', () => {
render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [library.items[0]] }}
installations={{
'makelore.notes': {
status: 'unavailable', pluginId: 'makelore.notes', reason: 'plugin_incompatible_client',
},
}}
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByText('尚未下载到设备')).toBeVisible();
expect(screen.getByRole('button', { name: '下载灵感笔记' })).toBeVisible();
expect(screen.queryByRole('button', { name: '删除设备上的灵感笔记' })).not.toBeInTheDocument();
expect(screen.queryByText('设备版本已是最新')).not.toBeInTheDocument();
expect(screen.getByText(/当前 MakeLore 版本不兼容/)).toBeVisible();
});
it('honestly groups stable Main failure codes without claiming hidden authority', () => {
const { rerender } = render(<MemoryRouter><MyPluginsView
library={{ ...library, items: [library.items[0]] }} installations={{}} state="ready"
error="plugin_artifact_invalid: Plugin artifact is invalid" pending={{}} onRefresh={vi.fn()}
onInstall={vi.fn()} onUpdate={vi.fn()} onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByRole('alert')).toHaveTextContent('签名或包校验失败');
rerender(<MemoryRouter><MyPluginsView
library={{ ...library, items: [library.items[0]] }} installations={{}} state="ready"
error="plugin_release_not_ready: Plugin Release is not ready" pending={{}} onRefresh={vi.fn()}
onInstall={vi.fn()} onUpdate={vi.fn()} onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
/></MemoryRouter>);
expect(screen.getByRole('alert')).toHaveTextContent('版本尚未就绪;不会替换此前可用版本');
});
});