Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

394 lines
12 KiB
TypeScript

// @vitest-environment jsdom
// PluginShareMenu — plugin actions affordance contract.
//
// Locks the popover behaviour users expect from a plugin-specific
// actions button on a detail modal: copy install command / plugin id /
// README badge land on the clipboard, and the popover surfaces source +
// homepage links when the manifest carries them.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import type { InstalledPluginRecord } from '@open-design/contracts';
import {
buildPluginShareUrl,
PluginShareMenu,
} from '../../src/components/plugin-details/PluginShareMenu';
import { I18nProvider, type Locale } from '../../src/i18n';
interface MakeArgs {
id: string;
title?: string;
source?: string;
sourceKind?: InstalledPluginRecord['sourceKind'];
marketplaceId?: string;
marketplaceEntryName?: string;
authorUrl?: string;
homepage?: string;
}
function make(args: MakeArgs): InstalledPluginRecord {
return {
id: args.id,
title: args.title ?? args.id,
version: '0.1.0',
sourceKind: args.sourceKind ?? 'bundled',
source: args.source ?? `plugins/${args.id}`,
sourceMarketplaceId: args.marketplaceId,
sourceMarketplaceEntryName: args.marketplaceEntryName,
trust: 'bundled',
capabilitiesGranted: [],
manifest: {
name: args.id,
version: '0.1.0',
title: args.title ?? args.id,
...(args.authorUrl ? { author: { url: args.authorUrl } } : {}),
...(args.homepage ? { homepage: args.homepage } : {}),
od: { kind: 'scenario' },
},
fsPath: '/tmp',
installedAt: 0,
updatedAt: 0,
};
}
describe('PluginShareMenu', () => {
let container: HTMLDivElement;
let root: Root;
let writes: string[];
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
writes = [];
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn(async (value: string) => {
writes.push(value);
}),
},
});
Object.defineProperty(window, 'location', {
configurable: true,
value: {
...window.location,
origin: 'https://example.test',
},
});
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
function renderMenu(record: InstalledPluginRecord, locale?: Locale) {
act(() => {
root.render(
locale ? (
<I18nProvider initial={locale}>
<PluginShareMenu record={record} />
</I18nProvider>
) : (
<PluginShareMenu record={record} />
),
);
});
}
function openPopover(expectedTriggerText = 'More') {
const trigger = container.querySelector(
'.plugin-share-trigger',
) as HTMLButtonElement;
expect(trigger).toBeTruthy();
expect(trigger.textContent).toContain(expectedTriggerText);
act(() => {
trigger.click();
});
}
function clickItem(label: string) {
const items = Array.from(
container.querySelectorAll('.plugin-share-item'),
) as HTMLButtonElement[];
const match = items.find((b) => b.textContent?.includes(label));
expect(match, `expected an item labelled "${label}"`).toBeTruthy();
act(() => {
match!.click();
});
}
it('copies an install command for marketplace plugins using the registry entry name', async () => {
renderMenu(
make({
id: 'mp-plugin',
sourceKind: 'github',
source: 'github:open-design/plugins/mp-plugin',
marketplaceId: 'official',
marketplaceEntryName: 'open-design/mp-plugin',
}),
);
openPopover();
clickItem('Copy install command');
await Promise.resolve();
expect(writes).toContain('od plugin install open-design/mp-plugin');
});
it('copies the github source string for github-installed plugins', async () => {
renderMenu(
make({
id: 'gh-plugin',
sourceKind: 'github',
source: 'github:owner/repo@main/sub',
}),
);
openPopover();
clickItem('Copy install command');
await Promise.resolve();
expect(writes).toContain('od plugin install github:owner/repo@main/sub');
});
it('does not duplicate the template share link action', () => {
renderMenu(make({ id: 'live-dashboard' }));
openPopover();
const labels = Array.from(
container.querySelectorAll('.plugin-share-item'),
).map((item) => item.textContent ?? '');
expect(labels.some((label) => label.includes('Copy share link'))).toBe(false);
});
it('copies the bare plugin id for paste-into-yaml workflows', async () => {
renderMenu(make({ id: 'agentic-ds' }));
openPopover();
clickItem('Copy plugin ID');
await Promise.resolve();
expect(writes).toContain('agentic-ds');
});
it('copies a README badge that links back to the marketplace detail page', async () => {
renderMenu(make({
id: 'badge-plugin',
title: 'Badge Plugin',
marketplaceId: 'official',
marketplaceEntryName: 'open-design/badge-plugin',
}));
openPopover();
clickItem('Copy README badge');
await Promise.resolve();
expect(writes.some((value) => (
value.includes('Badge Plugin') &&
value.includes('https://open-design.ai/plugins/badge-plugin')
))).toBe(true);
});
it('does not expose public share artifacts for local-only plugins', () => {
const localOnly = make({
id: 'local-plugin',
sourceKind: 'local',
source: '/tmp/local-plugin',
});
expect(buildPluginShareUrl(localOnly)).toBeNull();
renderMenu(localOnly);
openPopover();
const labels = Array.from(
container.querySelectorAll('.plugin-share-item'),
).map((item) => item.textContent ?? '');
expect(labels.some((label) => label.includes('Copy README badge'))).toBe(false);
});
it('does not expose public share artifacts for private marketplace plugins', () => {
const privateMarketplace = make({
id: 'private-plugin',
sourceKind: 'marketplace',
source: 'private/private-plugin',
marketplaceId: 'private',
marketplaceEntryName: 'private/private-plugin',
});
expect(buildPluginShareUrl(privateMarketplace)).toBeNull();
renderMenu(privateMarketplace);
openPopover();
const labels = Array.from(
container.querySelectorAll('.plugin-share-item'),
).map((item) => item.textContent ?? '');
expect(labels.some((label) => label.includes('Copy README badge'))).toBe(false);
});
it('localizes the plugin action menu labels', () => {
renderMenu(
make({
id: 'zh-plugin',
sourceKind: 'github',
source: 'github:owner/repo',
marketplaceId: 'official',
marketplaceEntryName: 'open-design/zh-plugin',
homepage: 'https://example.test/plugin-home',
}),
'zh-CN',
);
openPopover('更多');
const labels = Array.from(
container.querySelectorAll('.plugin-share-item'),
).map((item) => item.textContent ?? '');
expect(labels).toContain('复制安装命令');
expect(labels).toContain('复制插件 ID');
expect(labels).toContain('复制 README 徽章');
expect(labels).toContain('在 GitHub 打开源码');
expect(labels).toContain('打开项目主页');
expect(labels).toContain('在插件市场打开');
expect(labels.some((label) => label.includes('Copy install command'))).toBe(false);
});
it('points Open in marketplace at the public open-design.ai page for bundled plugins', () => {
renderMenu(make({ id: 'plain' }));
openPopover();
const items = Array.from(
container.querySelectorAll('.plugin-share-item'),
) as HTMLButtonElement[];
expect(items.some((b) => b.textContent?.includes('Open in marketplace'))).toBe(
true,
);
const marketplaceLink = Array.from(
container.querySelectorAll<HTMLAnchorElement>('a.plugin-share-item'),
).find((link) => link.textContent?.includes('Open in marketplace'));
// Bundled plugins have a public detail page, so the link is the public
// open-design.ai URL — not a local /marketplace path.
expect(marketplaceLink?.getAttribute('href')).toBe(
'https://open-design.ai/plugins/plain/',
);
});
it('builds a public open-design.ai share link for bundled plugins', () => {
expect(buildPluginShareUrl(make({ id: 'simple-deck' }))).toBe(
'https://open-design.ai/plugins/simple-deck/',
);
});
it('builds a public open-design.ai share link for community marketplace plugins', () => {
// Community manifest names carry a `community-` prefix, but the landing
// page routes are keyed on the folder name via routeId=`community/<folder>`.
// buildPluginShareUrl must use sourceMarketplaceEntryName so pluginDetailSlug
// takes the last segment and matches the generated page slug.
expect(
buildPluginShareUrl(
make({
id: 'community-registry-starter',
sourceKind: 'marketplace',
source: 'community/registry-starter',
marketplaceId: 'community',
marketplaceEntryName: 'community/registry-starter',
}),
),
).toBe('https://open-design.ai/plugins/registry-starter/');
});
it('copies a README badge for community marketplace plugins', async () => {
renderMenu(
make({
id: 'community-registry-starter',
title: 'Community Registry Starter',
sourceKind: 'marketplace',
source: 'community/registry-starter',
marketplaceId: 'community',
marketplaceEntryName: 'community/registry-starter',
}),
);
openPopover();
clickItem('Copy README badge');
await Promise.resolve();
expect(
writes.some(
(value) =>
value.includes('Community Registry Starter') &&
value.includes('https://open-design.ai/plugins/registry-starter/'),
),
).toBe(true);
});
it('points Open in marketplace at the public page for community marketplace plugins', () => {
renderMenu(
make({
id: 'community-registry-starter',
sourceKind: 'marketplace',
source: 'community/registry-starter',
marketplaceId: 'community',
marketplaceEntryName: 'community/registry-starter',
}),
);
openPopover();
const marketplaceLink = Array.from(
container.querySelectorAll<HTMLAnchorElement>('a.plugin-share-item'),
).find((link) => link.textContent?.includes('Open in marketplace'));
expect(marketplaceLink?.getAttribute('href')).toBe(
'https://open-design.ai/plugins/registry-starter/',
);
});
it('surfaces the GitHub source link when sourceKind is github', () => {
renderMenu(
make({
id: 'gh-with-link',
sourceKind: 'github',
source: 'github:owner/repo',
}),
);
openPopover();
const items = Array.from(
container.querySelectorAll('.plugin-share-item'),
) as HTMLElement[];
expect(items.some((b) => b.textContent?.includes('Open source on GitHub'))).toBe(
true,
);
const sourceLink = container.querySelector<HTMLAnchorElement>(
'a.plugin-share-item[href="https://github.com/owner/repo"]',
);
expect(sourceLink).toBeTruthy();
});
it('surfaces the homepage link when manifest.homepage is set', () => {
renderMenu(
make({
id: 'with-homepage',
sourceKind: 'local',
homepage: 'https://example.test/plugin-home',
}),
);
openPopover();
const items = Array.from(
container.querySelectorAll('.plugin-share-item'),
) as HTMLElement[];
expect(items.some((b) => b.textContent?.includes('Open homepage'))).toBe(
true,
);
const homepageLink = Array.from(
container.querySelectorAll<HTMLAnchorElement>('a.plugin-share-item'),
).find((link) => link.textContent?.includes('Open homepage'));
expect(homepageLink).toBeTruthy();
expect(homepageLink?.getAttribute('href')).toBe('https://example.test/plugin-home');
});
it('renders official bundled repo links as anchors', () => {
renderMenu(
make({
id: 'official-plugin',
sourceKind: 'bundled',
source: 'plugins/_official/scenarios/official-plugin',
}),
);
openPopover();
const repoLinks = Array.from(
container.querySelectorAll<HTMLAnchorElement>(
'a.plugin-share-item[href="https://github.com/nexu-io/open-design"]',
),
);
expect(repoLinks.length).toBeGreaterThan(0);
expect(
repoLinks.some((link) => link.textContent?.includes('Open source on GitHub')),
).toBe(true);
});
});