070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
787 lines
28 KiB
TypeScript
787 lines
28 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { InstalledPluginRecord, PluginSourceKind, TrustTier } from '@open-design/contracts';
|
|
import { PluginsView } from '../../src/components/PluginsView';
|
|
import {
|
|
addPluginMarketplace,
|
|
applyPlugin,
|
|
installPluginSource,
|
|
listPluginMarketplaces,
|
|
listPlugins,
|
|
refreshPluginMarketplace,
|
|
removePluginMarketplace,
|
|
setPluginMarketplaceTrust,
|
|
type PluginShareProjectOutcome,
|
|
uploadPluginFolder,
|
|
uploadPluginZip,
|
|
} from '../../src/state/projects';
|
|
|
|
vi.mock('../../src/router', () => ({
|
|
navigate: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../src/state/projects', () => ({
|
|
addPluginMarketplace: vi.fn(),
|
|
applyPlugin: vi.fn(),
|
|
installPluginSource: vi.fn(),
|
|
listPluginMarketplaces: vi.fn(),
|
|
listPlugins: vi.fn(),
|
|
refreshPluginMarketplace: vi.fn(),
|
|
removePluginMarketplace: vi.fn(),
|
|
setPluginMarketplaceTrust: vi.fn(),
|
|
uninstallPlugin: vi.fn(),
|
|
uploadPluginFolder: vi.fn(),
|
|
uploadPluginZip: vi.fn(),
|
|
upgradePlugin: vi.fn(),
|
|
}));
|
|
|
|
function makePlugin(
|
|
id: string,
|
|
sourceKind: PluginSourceKind,
|
|
trust: TrustTier,
|
|
title = id === 'official-plugin' ? 'Official Plugin' : 'User Plugin',
|
|
description = `${id} description`,
|
|
): InstalledPluginRecord {
|
|
const record: InstalledPluginRecord = {
|
|
id,
|
|
title,
|
|
version: '1.0.0',
|
|
sourceKind,
|
|
source: '/tmp',
|
|
trust,
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
manifest: {
|
|
name: id,
|
|
version: '1.0.0',
|
|
title: id,
|
|
description,
|
|
od: {
|
|
kind: 'scenario',
|
|
mode: 'prototype',
|
|
},
|
|
},
|
|
fsPath: '/tmp',
|
|
installedAt: 0,
|
|
updatedAt: 0,
|
|
};
|
|
if (sourceKind === 'bundled') {
|
|
record.sourceMarketplaceId = 'official';
|
|
record.sourceMarketplaceEntryName = `open-design/${id}`;
|
|
record.sourceMarketplaceEntryVersion = record.version;
|
|
record.marketplaceTrust = 'official';
|
|
}
|
|
return record;
|
|
}
|
|
|
|
const mockedListPlugins = vi.mocked(listPlugins);
|
|
const mockedListMarketplaces = vi.mocked(listPluginMarketplaces);
|
|
const mockedInstallPluginSource = vi.mocked(installPluginSource);
|
|
const mockedAddMarketplace = vi.mocked(addPluginMarketplace);
|
|
const mockedRefreshMarketplace = vi.mocked(refreshPluginMarketplace);
|
|
const mockedRemoveMarketplace = vi.mocked(removePluginMarketplace);
|
|
const mockedSetMarketplaceTrust = vi.mocked(setPluginMarketplaceTrust);
|
|
const mockedApplyPlugin = vi.mocked(applyPlugin);
|
|
const mockedUploadPluginFolder = vi.mocked(uploadPluginFolder);
|
|
const mockedUploadPluginZip = vi.mocked(uploadPluginZip);
|
|
|
|
beforeEach(() => {
|
|
mockedListPlugins.mockResolvedValue([
|
|
makePlugin('official-plugin', 'bundled', 'bundled'),
|
|
makePlugin('user-plugin', 'github', 'restricted'),
|
|
]);
|
|
mockedListMarketplaces.mockResolvedValue([
|
|
{
|
|
id: 'catalog-1',
|
|
url: 'https://example.com/open-design-marketplace.json',
|
|
trust: 'official',
|
|
manifest: {
|
|
name: 'Example Catalog',
|
|
version: '1.0.0',
|
|
plugins: [
|
|
{
|
|
name: 'remote-plugin',
|
|
title: 'Remote Plugin',
|
|
source: 'github:owner/repo',
|
|
version: '1.2.0',
|
|
description: 'Remote catalog plugin.',
|
|
tags: ['deck'],
|
|
},
|
|
{
|
|
name: 'open-design/official-plugin',
|
|
title: 'Official Plugin',
|
|
title_i18n: { 'zh-CN': '官方看板' },
|
|
source: 'github:nexu-io/open-design@main/plugins/_official/examples/official-plugin',
|
|
version: '1.0.0',
|
|
description: 'Bundled official plugin.',
|
|
description_i18n: { 'zh-CN': '内置官方插件。' },
|
|
tags: ['prototype', 'kanban'],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
mockedAddMarketplace.mockResolvedValue({
|
|
ok: true,
|
|
message: 'Marketplace source added.',
|
|
});
|
|
mockedRefreshMarketplace.mockResolvedValue({
|
|
ok: true,
|
|
message: 'Marketplace source refreshed.',
|
|
});
|
|
mockedRemoveMarketplace.mockResolvedValue({
|
|
ok: true,
|
|
message: 'Marketplace source removed.',
|
|
});
|
|
mockedSetMarketplaceTrust.mockResolvedValue({
|
|
ok: true,
|
|
message: 'Marketplace trust updated.',
|
|
});
|
|
mockedInstallPluginSource.mockResolvedValue({
|
|
ok: true,
|
|
plugin: makePlugin('new-plugin', 'github', 'restricted'),
|
|
warnings: [],
|
|
message: 'Installed New Plugin.',
|
|
log: ['Parsing manifest'],
|
|
});
|
|
mockedUploadPluginZip.mockResolvedValue({
|
|
ok: true,
|
|
plugin: makePlugin('zip-plugin', 'user', 'restricted'),
|
|
warnings: [],
|
|
message: 'Installed Zip Plugin.',
|
|
log: [],
|
|
});
|
|
mockedUploadPluginFolder.mockResolvedValue({
|
|
ok: true,
|
|
plugin: makePlugin('folder-plugin', 'user', 'restricted'),
|
|
warnings: [],
|
|
message: 'Installed Folder Plugin.',
|
|
log: [],
|
|
});
|
|
mockedApplyPlugin.mockResolvedValue({
|
|
query: 'Make something.',
|
|
contextItems: [],
|
|
inputs: [],
|
|
assets: [],
|
|
mcpServers: [],
|
|
trust: 'restricted',
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
capabilitiesRequired: ['prompt:inject'],
|
|
appliedPlugin: {
|
|
snapshotId: 'snap-1',
|
|
pluginId: 'official-plugin',
|
|
pluginVersion: '1.0.0',
|
|
manifestSourceDigest: 'a'.repeat(64),
|
|
inputs: {},
|
|
resolvedContext: { items: [] },
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
capabilitiesRequired: ['prompt:inject'],
|
|
assetsStaged: [],
|
|
taskKind: 'new-generation',
|
|
appliedAt: 0,
|
|
connectorsRequired: [],
|
|
connectorsResolved: [],
|
|
mcpServers: [],
|
|
status: 'fresh',
|
|
},
|
|
projectMetadata: {},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('PluginsView', () => {
|
|
it('starts guided plugin creation from the Plugins hero', async () => {
|
|
const onCreatePlugin = vi.fn();
|
|
render(<PluginsView onCreatePlugin={onCreatePlugin} />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-create-button'));
|
|
|
|
expect(onCreatePlugin).toHaveBeenCalledTimes(1);
|
|
expect(screen.queryByRole('dialog', { name: 'Import a plugin' })).toBeNull();
|
|
});
|
|
|
|
it('shows installed plugins and available registry entries', async () => {
|
|
render(<PluginsView />);
|
|
|
|
expect(await screen.findByText('Installed plugins')).toBeTruthy();
|
|
expect(screen.getAllByText('User Plugin').length).toBeGreaterThan(0);
|
|
expect(screen.queryByText('Official Plugin')).toBeNull();
|
|
|
|
const availableTab = screen.getByTestId('plugins-tab-available');
|
|
const sourcesTab = screen.getByTestId('plugins-tab-sources');
|
|
expect(availableTab.getAttribute('aria-disabled')).toBeNull();
|
|
expect(sourcesTab.getAttribute('aria-disabled')).toBeNull();
|
|
|
|
fireEvent.click(availableTab);
|
|
expect(await screen.findByText('Remote Plugin')).toBeTruthy();
|
|
expect(screen.getAllByText('Example Catalog').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('keeps bundled official catalog entries available and uses the installed record', async () => {
|
|
const onUsePlugin = vi.fn();
|
|
mockedListMarketplaces.mockResolvedValue([
|
|
{
|
|
id: 'official',
|
|
url: 'https://open-design.ai/marketplace/open-design-marketplace.json',
|
|
trust: 'official',
|
|
manifest: {
|
|
name: 'Open Design Official',
|
|
version: '1.0.0',
|
|
plugins: [
|
|
{
|
|
name: 'open-design/official-plugin',
|
|
title: 'Official Plugin',
|
|
title_i18n: { 'zh-CN': '官方看板' },
|
|
source: 'github:nexu-io/open-design@main/plugins/_official/examples/official-plugin',
|
|
version: '1.0.0',
|
|
description: 'Bundled official plugin.',
|
|
description_i18n: { 'zh-CN': '内置官方插件。' },
|
|
tags: ['prototype', 'kanban'],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
render(<PluginsView onUsePlugin={onUsePlugin} />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
expect(await screen.findByText('官方看板')).toBeTruthy();
|
|
|
|
fireEvent.change(screen.getByLabelText('Search available plugins'), {
|
|
target: { value: '官方看板' },
|
|
});
|
|
|
|
expect(await screen.findByText('官方看板')).toBeTruthy();
|
|
expect(screen.queryByText('Remote Plugin')).toBeNull();
|
|
|
|
fireEvent.click(screen.getByTestId('plugins-available-install-open-design/official-plugin'));
|
|
|
|
expect(onUsePlugin).toHaveBeenCalledWith(expect.objectContaining({
|
|
id: 'official-plugin',
|
|
sourceKind: 'bundled',
|
|
}), 'use');
|
|
expect(mockedInstallPluginSource).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('installs restricted catalog entries that collide with bundled official plugin names', async () => {
|
|
const onUsePlugin = vi.fn();
|
|
mockedListMarketplaces.mockResolvedValue([
|
|
{
|
|
id: 'team-catalog',
|
|
url: 'https://team.example.com/open-design-marketplace.json',
|
|
trust: 'restricted',
|
|
manifest: {
|
|
name: 'Team Catalog',
|
|
version: '1.0.0',
|
|
plugins: [
|
|
{
|
|
name: 'open-design/official-plugin',
|
|
title: 'Team Official Plugin',
|
|
source: 'github:team/official-plugin',
|
|
version: '2.0.0',
|
|
description: 'Team-scoped plugin that intentionally shares the official entry name.',
|
|
tags: ['team'],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
|
|
render(<PluginsView onUsePlugin={onUsePlugin} />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
expect(await screen.findByText('Team Official Plugin')).toBeTruthy();
|
|
|
|
const install = screen.getByTestId('plugins-available-install-open-design/official-plugin');
|
|
expect(install.textContent).toBe('Install');
|
|
fireEvent.click(install);
|
|
|
|
await waitFor(() =>
|
|
expect(mockedInstallPluginSource).toHaveBeenCalledWith('open-design/official-plugin'),
|
|
);
|
|
expect(onUsePlugin).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('shows all installed plugins by default on the Plugins page', async () => {
|
|
const createPlugin = makePlugin('create-plugin', 'github', 'restricted', 'Create Plugin');
|
|
const importPlugin = makePlugin('import-plugin', 'github', 'restricted', 'Import Plugin');
|
|
importPlugin.manifest.od = {
|
|
kind: 'scenario',
|
|
mode: 'scenario',
|
|
taskKind: 'figma-migration',
|
|
};
|
|
mockedListPlugins.mockResolvedValue([createPlugin, importPlugin]);
|
|
mockedListMarketplaces.mockResolvedValue([]);
|
|
|
|
render(<PluginsView />);
|
|
|
|
const list = await screen.findByRole('list');
|
|
expect(
|
|
within(list)
|
|
.getAllByRole('listitem')
|
|
.map((item) => item.getAttribute('data-plugin-id'))
|
|
.sort(),
|
|
).toEqual(['create-plugin', 'import-plugin']);
|
|
const summary = screen.getByLabelText('Plugin summary');
|
|
expect(within(summary).getByText('2')).toBeTruthy();
|
|
expect(within(summary).getByText('Installed')).toBeTruthy();
|
|
});
|
|
|
|
it('hands installed plugin Use actions to the host shell', async () => {
|
|
const onUsePlugin = vi.fn();
|
|
render(<PluginsView onUsePlugin={onUsePlugin} />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-home-use-user-plugin'));
|
|
|
|
expect(onUsePlugin).toHaveBeenCalledWith(expect.objectContaining({
|
|
id: 'user-plugin',
|
|
title: 'User Plugin',
|
|
}), 'use');
|
|
expect(mockedApplyPlugin).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('hands Use with query actions to the host shell', async () => {
|
|
const onUsePlugin = vi.fn();
|
|
const user = makePlugin('query-plugin', 'github', 'restricted', 'Query Plugin');
|
|
user.manifest.od = {
|
|
...user.manifest.od,
|
|
useCase: { query: 'Make a query-backed artifact.' },
|
|
};
|
|
mockedListPlugins.mockResolvedValue([user]);
|
|
mockedListMarketplaces.mockResolvedValue([]);
|
|
|
|
render(<PluginsView onUsePlugin={onUsePlugin} />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-home-use-menu-query-plugin'));
|
|
fireEvent.click(screen.getByTestId('plugins-home-use-with-query-query-plugin'));
|
|
|
|
expect(onUsePlugin).toHaveBeenCalledWith(expect.objectContaining({
|
|
id: 'query-plugin',
|
|
title: 'Query Plugin',
|
|
}), 'use-with-query');
|
|
expect(mockedApplyPlugin).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('installs from a supported source string', async () => {
|
|
render(<PluginsView />);
|
|
|
|
expect(screen.queryByTestId('plugins-tab-import')).toBeNull();
|
|
fireEvent.click(await screen.findByTestId('plugins-import-button'));
|
|
expect(screen.getByRole('dialog', { name: 'Import a plugin' })).toBeTruthy();
|
|
expect(screen.queryByText('Create from template')).toBeNull();
|
|
const source = 'github:nexu-io/open-design@garnet-hemisphere/plugins/community/registry-starter';
|
|
fireEvent.change(screen.getByLabelText('GitHub, archive, or marketplace source'), {
|
|
target: { value: source },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: 'Import' }));
|
|
|
|
await waitFor(() =>
|
|
expect(mockedInstallPluginSource).toHaveBeenCalledWith(
|
|
source,
|
|
),
|
|
);
|
|
expect(await screen.findByText('Installed New Plugin.')).toBeTruthy();
|
|
expect(screen.getByTestId('plugins-tab-installed').getAttribute('aria-selected')).toBe('true');
|
|
expect(screen.getAllByText('User Plugin').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('installs an available marketplace entry by name', async () => {
|
|
render(<PluginsView />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
fireEvent.click(await screen.findByTestId('plugins-available-install-remote-plugin'));
|
|
|
|
await waitFor(() =>
|
|
expect(mockedInstallPluginSource).toHaveBeenCalledWith('remote-plugin'),
|
|
);
|
|
expect(await screen.findByText('Installed New Plugin.')).toBeTruthy();
|
|
expect(screen.getByTestId('plugins-tab-installed').getAttribute('aria-selected')).toBe('true');
|
|
});
|
|
|
|
it('opens details for available marketplace entries', async () => {
|
|
render(<PluginsView />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
fireEvent.click(await screen.findByTestId('plugins-available-details-remote-plugin'));
|
|
|
|
const dialog = await screen.findByTestId('plugins-available-details-modal');
|
|
expect(within(dialog).getByText('Remote Plugin')).toBeTruthy();
|
|
expect(dialog.textContent).toContain('Remote catalog plugin.');
|
|
expect(dialog.textContent).toContain('github:owner/repo');
|
|
|
|
fireEvent.click(within(dialog).getByTestId('plugins-available-details-install-remote-plugin'));
|
|
|
|
await waitFor(() =>
|
|
expect(mockedInstallPluginSource).toHaveBeenCalledWith('remote-plugin@1.2.0'),
|
|
);
|
|
expect(await screen.findByText('Installed New Plugin.')).toBeTruthy();
|
|
await waitFor(() =>
|
|
expect(screen.queryByTestId('plugins-available-details-modal')).toBeNull(),
|
|
);
|
|
});
|
|
|
|
it('shows registry provenance, permissions, versions, and copyable install command in available details', async () => {
|
|
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
Object.defineProperty(navigator, 'clipboard', {
|
|
configurable: true,
|
|
value: { writeText },
|
|
});
|
|
mockedListMarketplaces.mockResolvedValue([
|
|
{
|
|
id: 'official',
|
|
url: 'https://example.com/open-design-marketplace.json',
|
|
trust: 'official',
|
|
manifest: {
|
|
name: 'Official Registry',
|
|
version: '1.0.0',
|
|
plugins: [{
|
|
name: 'remote-plugin',
|
|
title: 'Remote Plugin',
|
|
source: 'github:owner/repo',
|
|
version: '1.2.0',
|
|
ref: 'v1.2.0',
|
|
integrity: 'sha256:latest',
|
|
description: 'Remote catalog plugin.',
|
|
permissions: ['prompt:inject', 'fs:read'],
|
|
capabilitiesSummary: ['Injects prompt context', 'Reads local design assets'],
|
|
versions: [
|
|
{
|
|
version: '1.2.0',
|
|
source: 'github:owner/repo',
|
|
ref: 'v1.2.0',
|
|
integrity: 'sha256:latest',
|
|
},
|
|
{
|
|
version: '1.1.0',
|
|
source: 'github:owner/repo',
|
|
ref: 'v1.1.0',
|
|
integrity: 'sha256:previous',
|
|
},
|
|
],
|
|
}],
|
|
},
|
|
},
|
|
]);
|
|
render(<PluginsView />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
fireEvent.click(await screen.findByTestId('plugins-available-details-remote-plugin'));
|
|
|
|
const dialog = await screen.findByTestId('plugins-available-details-modal');
|
|
expect(within(dialog).getByTestId('plugins-available-provenance').textContent)
|
|
.toContain('from Official Registry · official · github:owner/repo@v1.2.0 · sha256:latest');
|
|
expect(within(dialog).getByText('Permissions')).toBeTruthy();
|
|
expect(within(dialog).getByText('prompt:inject')).toBeTruthy();
|
|
expect(within(dialog).getByText('fs:read')).toBeTruthy();
|
|
expect(within(dialog).getByText('Capability summary')).toBeTruthy();
|
|
expect(within(dialog).getByText('Injects prompt context')).toBeTruthy();
|
|
|
|
fireEvent.change(within(dialog).getByLabelText('Plugin version'), {
|
|
target: { value: '1.1.0' },
|
|
});
|
|
expect(within(dialog).getByTestId('plugins-available-install-command').textContent)
|
|
.toContain('od plugin install remote-plugin@1.1.0');
|
|
expect(within(dialog).getByTestId('plugins-available-provenance').textContent)
|
|
.toContain('github:owner/repo@v1.1.0 · sha256:previous');
|
|
|
|
fireEvent.click(within(dialog).getByRole('button', { name: 'Copy install command' }));
|
|
await waitFor(() =>
|
|
expect(writeText).toHaveBeenCalledWith('od plugin install remote-plugin@1.1.0'),
|
|
);
|
|
|
|
fireEvent.click(within(dialog).getByTestId('plugins-available-details-install-remote-plugin'));
|
|
await waitFor(() =>
|
|
expect(mockedInstallPluginSource).toHaveBeenCalledWith('remote-plugin@1.1.0'),
|
|
);
|
|
});
|
|
|
|
it('does not inherit latest ref and integrity for older sparse available versions', async () => {
|
|
mockedListMarketplaces.mockResolvedValue([
|
|
{
|
|
id: 'official',
|
|
url: 'https://example.com/open-design-marketplace.json',
|
|
trust: 'official',
|
|
manifest: {
|
|
name: 'Official Registry',
|
|
version: '1.0.0',
|
|
plugins: [{
|
|
name: 'remote-plugin',
|
|
title: 'Remote Plugin',
|
|
source: 'github:owner/repo',
|
|
version: '1.2.0',
|
|
ref: 'v1.2.0',
|
|
integrity: 'sha256:latest',
|
|
description: 'Remote catalog plugin.',
|
|
versions: [
|
|
{ version: '1.2.0' },
|
|
{ version: '1.1.0' },
|
|
],
|
|
}],
|
|
},
|
|
},
|
|
]);
|
|
render(<PluginsView />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
fireEvent.click(await screen.findByTestId('plugins-available-details-remote-plugin'));
|
|
|
|
const dialog = await screen.findByTestId('plugins-available-details-modal');
|
|
expect(within(dialog).getByTestId('plugins-available-provenance').textContent)
|
|
.toContain('github:owner/repo@v1.2.0 · sha256:latest');
|
|
|
|
fireEvent.change(within(dialog).getByLabelText('Plugin version'), {
|
|
target: { value: '1.1.0' },
|
|
});
|
|
|
|
expect(within(dialog).getByTestId('plugins-available-provenance').textContent)
|
|
.toContain('from Official Registry · official · github:owner/repo');
|
|
expect(within(dialog).getByTestId('plugins-available-provenance').textContent)
|
|
.not.toContain('v1.2.0');
|
|
expect(within(dialog).getByTestId('plugins-available-provenance').textContent)
|
|
.not.toContain('sha256:latest');
|
|
expect(within(dialog).queryByText('Ref')).toBeNull();
|
|
expect(within(dialog).queryByText('Integrity')).toBeNull();
|
|
});
|
|
|
|
it('filters available marketplace entries by source and search', async () => {
|
|
mockedListMarketplaces.mockResolvedValue([
|
|
{
|
|
id: 'catalog-1',
|
|
url: 'https://example.com/open-design-marketplace.json',
|
|
trust: 'official',
|
|
manifest: {
|
|
name: 'Example Catalog',
|
|
version: '1.0.0',
|
|
plugins: [{
|
|
name: 'remote-plugin',
|
|
title: 'Remote Plugin',
|
|
source: 'github:owner/repo',
|
|
version: '1.2.0',
|
|
description: 'Remote catalog plugin.',
|
|
tags: ['deck'],
|
|
}],
|
|
},
|
|
},
|
|
{
|
|
id: 'catalog-2',
|
|
url: 'https://team.example.com/open-design-marketplace.json',
|
|
trust: 'restricted',
|
|
manifest: {
|
|
name: 'Team Catalog',
|
|
version: '1.0.0',
|
|
plugins: [{
|
|
name: 'figma-importer',
|
|
title: 'Figma Importer',
|
|
source: 'github:team/figma-importer',
|
|
version: '0.2.0',
|
|
description: 'Imports a Figma source.',
|
|
tags: ['figma', 'import'],
|
|
}],
|
|
},
|
|
},
|
|
]);
|
|
|
|
render(<PluginsView />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
expect(await screen.findByText('Remote Plugin')).toBeTruthy();
|
|
expect(screen.getByText('Figma Importer')).toBeTruthy();
|
|
|
|
fireEvent.change(screen.getByLabelText('Search available plugins'), {
|
|
target: { value: 'figma' },
|
|
});
|
|
|
|
expect(await screen.findByText('Figma Importer')).toBeTruthy();
|
|
expect(screen.queryByText('Remote Plugin')).toBeNull();
|
|
|
|
fireEvent.change(screen.getByLabelText('Source'), {
|
|
target: { value: 'catalog-1' },
|
|
});
|
|
|
|
expect(await screen.findByText('No available entries match your filters.')).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Clear available plugin search' }));
|
|
|
|
expect(await screen.findByText('Remote Plugin')).toBeTruthy();
|
|
expect(screen.queryByText('Figma Importer')).toBeNull();
|
|
});
|
|
|
|
it('keeps non-bundled installed registry entries out of Available', async () => {
|
|
const marketplacePlugin = makePlugin(
|
|
'marketplace-plugin',
|
|
'marketplace',
|
|
'restricted',
|
|
'Marketplace Plugin',
|
|
);
|
|
marketplacePlugin.sourceMarketplaceId = 'official';
|
|
marketplacePlugin.sourceMarketplaceEntryName = 'open-design/official-plugin';
|
|
marketplacePlugin.sourceMarketplaceEntryVersion = '1.0.0';
|
|
marketplacePlugin.marketplaceTrust = 'official';
|
|
marketplacePlugin.manifest.od = { ...marketplacePlugin.manifest.od, hidden: true };
|
|
mockedListPlugins.mockImplementation(async (options?: { includeHidden?: boolean }) =>
|
|
options?.includeHidden ? [marketplacePlugin] : [],
|
|
);
|
|
mockedListMarketplaces.mockResolvedValue([
|
|
{
|
|
id: 'official',
|
|
url: 'https://open-design.ai/marketplace/open-design-marketplace.json',
|
|
trust: 'official',
|
|
manifest: {
|
|
name: 'Open Design Official',
|
|
version: '0.1.0',
|
|
plugins: [{
|
|
name: 'open-design/official-plugin',
|
|
title: 'Official Plugin',
|
|
source: 'github:nexu-io/open-design@main/plugins/_official/scenarios/official-plugin',
|
|
version: '1.0.0',
|
|
description: 'Bundled official starter.',
|
|
tags: ['official'],
|
|
}],
|
|
},
|
|
},
|
|
]);
|
|
|
|
render(<PluginsView />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-available'));
|
|
expect(await screen.findByText(/Installed catalog entries are removed from Available/i)).toBeTruthy();
|
|
expect(screen.queryByText('Official Plugin')).toBeNull();
|
|
expect(screen.queryByRole('button', { name: 'Install' })).toBeNull();
|
|
expect(mockedListPlugins).toHaveBeenCalledWith({ includeHidden: true });
|
|
expect(mockedApplyPlugin).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('manages registry sources from the Sources tab', async () => {
|
|
render(<PluginsView />);
|
|
|
|
const sourceUrl =
|
|
'https://raw.githubusercontent.com/nexu-io/open-design/main/plugins/registry/community/open-design-marketplace.json';
|
|
fireEvent.click(await screen.findByTestId('plugins-tab-sources'));
|
|
fireEvent.change(screen.getByLabelText('Source URL'), {
|
|
target: { value: sourceUrl },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: 'Add source' }));
|
|
|
|
await waitFor(() =>
|
|
expect(mockedAddMarketplace).toHaveBeenCalledWith({
|
|
url: sourceUrl,
|
|
trust: 'restricted',
|
|
}),
|
|
);
|
|
expect(await screen.findByText('Marketplace source added.')).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
|
|
await waitFor(() => expect(mockedRefreshMarketplace).toHaveBeenCalledWith('catalog-1'));
|
|
|
|
fireEvent.change(screen.getByLabelText('Trust for Example Catalog'), {
|
|
target: { value: 'trusted' },
|
|
});
|
|
await waitFor(() =>
|
|
expect(mockedSetMarketplaceTrust).toHaveBeenCalledWith('catalog-1', 'trusted'),
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Remove' }));
|
|
await waitFor(() => expect(mockedRemoveMarketplace).toHaveBeenCalledWith('catalog-1'));
|
|
});
|
|
|
|
it('uploads zip and folder plugins from the import dialog', async () => {
|
|
render(<PluginsView />);
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-import-button'));
|
|
fireEvent.click(screen.getByRole('button', { name: /upload zip/i }));
|
|
const zip = new File(['zip-bytes'], 'plugin.zip', { type: 'application/zip' });
|
|
fireEvent.change(screen.getByTestId('plugins-zip-input'), {
|
|
target: { files: [zip] },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: 'Import' }));
|
|
await waitFor(() => expect(mockedUploadPluginZip).toHaveBeenCalledWith(zip));
|
|
expect(await screen.findByText('Installed Zip Plugin.')).toBeTruthy();
|
|
|
|
fireEvent.click(await screen.findByTestId('plugins-import-button'));
|
|
fireEvent.click(screen.getByRole('button', { name: /upload folder/i }));
|
|
const folderFile = new File(['{}'], 'open-design.json', { type: 'application/json' });
|
|
fireEvent.change(screen.getByTestId('plugins-folder-input'), {
|
|
target: { files: [folderFile] },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: 'Import' }));
|
|
await waitFor(() => expect(mockedUploadPluginFolder).toHaveBeenCalledWith([folderFile]));
|
|
expect(await screen.findByText('Installed Folder Plugin.')).toBeTruthy();
|
|
});
|
|
|
|
it('confirms a plugin share action before starting the GitHub repo task', async () => {
|
|
mockedListPlugins.mockResolvedValue([
|
|
makePlugin('official-plugin', 'bundled', 'bundled'),
|
|
makePlugin('user-plugin', 'github', 'restricted'),
|
|
makePlugin(
|
|
'od-plugin-publish-github',
|
|
'bundled',
|
|
'bundled',
|
|
'Publish Plugin to GitHub',
|
|
'Creates a public GitHub repository for a local Open Design plugin using the GitHub CLI.',
|
|
),
|
|
makePlugin(
|
|
'od-plugin-contribute-open-design',
|
|
'bundled',
|
|
'bundled',
|
|
'Contribute Plugin to Open Design',
|
|
'Opens a pull request that adds a local Open Design plugin to the Open Design community catalog.',
|
|
),
|
|
]);
|
|
const onCreatePluginShareProject = vi.fn(async (): Promise<PluginShareProjectOutcome> => ({
|
|
ok: true as const,
|
|
project: {
|
|
id: 'share-project',
|
|
name: 'Publish to GitHub: User Plugin',
|
|
skillId: null,
|
|
designSystemId: null,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
pendingPrompt: 'Publish it',
|
|
metadata: { kind: 'prototype' },
|
|
},
|
|
conversationId: 'conversation-1',
|
|
appliedPluginSnapshotId: 'snapshot-1',
|
|
actionPluginId: 'od-plugin-publish-github',
|
|
sourcePluginId: 'user-plugin',
|
|
stagedPath: 'plugin-source/user-plugin',
|
|
prompt: 'Publish it',
|
|
message: 'Created a Publish to GitHub task.',
|
|
}));
|
|
|
|
render(
|
|
<PluginsView
|
|
onCreatePluginShareProject={onCreatePluginShareProject}
|
|
/>,
|
|
);
|
|
|
|
const publish = await screen.findByTestId('plugins-home-publish-github-user-plugin');
|
|
expect(publish.textContent).toContain('Publish');
|
|
fireEvent.click(publish);
|
|
|
|
const dialog = await screen.findByRole('dialog', {
|
|
name: /Publish Plugin to GitHub for User Plugin/i,
|
|
});
|
|
expect(dialog.textContent).toContain('Creates a public GitHub repository');
|
|
expect(dialog.textContent).toContain('plugin-source/user-plugin');
|
|
expect(onCreatePluginShareProject).not.toHaveBeenCalled();
|
|
|
|
fireEvent.click(within(dialog).getByTestId('plugin-share-confirm-start'));
|
|
|
|
await waitFor(() =>
|
|
expect(onCreatePluginShareProject).toHaveBeenCalledWith(
|
|
'user-plugin',
|
|
'publish-github',
|
|
'en',
|
|
),
|
|
);
|
|
await waitFor(() =>
|
|
expect(screen.queryByTestId('plugin-share-confirm-modal')).toBeNull(),
|
|
);
|
|
});
|
|
});
|