mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* Reapply "fix(web): demote Plugins and Integrations to nav rail footer (#1806)" (#2360)
This reverts commit 1ab8758045.
* fixup: align EntryHelpMenu Discord URL with #2386 update
The revert of #2360 brought back EntryHelpMenu.tsx as #1806 originally
added it, with DISCORD_URL = 'https://discord.gg/BYShPgWpq'. #2386 later
rotated the Discord invite to mHAjSMV6gz, but only in the places that
existed on main at the time (EntryShell avatar dropdown + the e2e test);
EntryHelpMenu didn't exist then, so it never got updated. The e2e test
the revert reintroduced asserts the new URL, so the component must
match.
677 lines
24 KiB
TypeScript
677 lines
24 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 {
|
|
return {
|
|
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,
|
|
};
|
|
}
|
|
|
|
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'],
|
|
}],
|
|
},
|
|
},
|
|
]);
|
|
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('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 installed registry entries out of Available', async () => {
|
|
const official = makePlugin('official-plugin', 'bundled', 'bundled', 'Official Plugin');
|
|
official.sourceMarketplaceId = 'official';
|
|
official.sourceMarketplaceEntryName = 'open-design/official-plugin';
|
|
official.sourceMarketplaceEntryVersion = '1.0.0';
|
|
official.marketplaceTrust = 'official';
|
|
official.manifest.od = { ...official.manifest.od, hidden: true };
|
|
mockedListPlugins.mockImplementation(async (options?: { includeHidden?: boolean }) =>
|
|
options?.includeHidden ? [official] : [],
|
|
);
|
|
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(),
|
|
);
|
|
});
|
|
});
|