233 lines
14 KiB
TypeScript
233 lines
14 KiB
TypeScript
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');
|
||
expect(screen.getAllByRole('link', { name: '启用到项目' }).length).toBeGreaterThan(0);
|
||
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('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('版本尚未就绪;不会替换此前可用版本');
|
||
});
|
||
});
|