open-design/apps/web/tests/components/PluginExampleDetail.unavailable-noun.test.tsx
Hashem Aldhaheri bbf4809a7e
fix(web): use surface-appropriate noun in plugin/template preview unavailable copy (#3229)
After #2840 wired plugin and design-template 404s into the same
"no shipped preview" placeholder the skills tab uses, the placeholder
copy still hard-coded "skill" — so users opening a Community/Plugins
card whose manifest declares a preview entry that doesn't ship saw
"No shipped preview for this skill." on a card that is clearly not a
skill.

Adds a noun discriminator to PreviewView.unavailable so the placeholder
reads with the right word per surface — "this skill" on the Skills
tab, "this plugin" on Community/Plugins, "this template" on deck-mode
design-templates. Locales gain three new preview.noun* strings (with
appropriate per-language demonstrative+article) and the existing
unavailable title/body interpolate a {noun} placeholder.

Also fixes a CSS gap in .ds-modal-unavailable surfaced by the same
path: the title and body divs were collapsing onto a single line under
.ds-modal-empty's default flex-row. Mirrors the existing
.ds-modal-error column+gap layout.

Refs #897, #2840.
2026-05-29 03:23:18 +00:00

97 lines
3.3 KiB
TypeScript

// @vitest-environment jsdom
// Regression for nexu-io/open-design#3216: after #2840 wired plugin and
// design-template 404s into the same "no shipped preview" placeholder the
// skills tab uses, the placeholder copy still hard-coded "skill" — so a user
// opening a Community/Plugins card whose manifest declares a preview entry
// that doesn't ship saw "No shipped preview for this skill." on a card that
// is clearly not a skill. Lock the noun-per-surface contract by asserting
// that PluginExampleDetail's unavailable copy reads with the right noun.
//
// Plugin records (non-deck) read "plugin". Deck-mode records read "template".
// The skills consumer (ExamplesTab) keeps the existing "skill" wording and is
// covered by the existing preview-modal-unavailable-state suite.
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { InstalledPluginRecord } from '@open-design/contracts';
import { PluginExampleDetail } from '../../src/components/plugin-details/PluginExampleDetail';
vi.mock('../../src/providers/registry', () => ({
fetchPluginPreviewHtml: vi.fn(async () => ({ unavailable: true, kind: 'html' })),
fetchPluginExampleHtml: vi.fn(async () => ({ unavailable: true, kind: 'html' })),
}));
function make(overrides: {
id: string;
title?: string;
mode?: string;
}): InstalledPluginRecord {
return {
id: overrides.id,
title: overrides.title ?? overrides.id,
version: '0.1.0',
sourceKind: 'bundled',
source: '/tmp',
trust: 'bundled',
capabilitiesGranted: [],
manifest: {
name: overrides.id,
version: '0.1.0',
title: overrides.title ?? overrides.id,
od: {
kind: 'scenario',
...(overrides.mode ? { mode: overrides.mode } : {}),
preview: { type: 'html', entry: './missing.html' },
},
},
fsPath: '/tmp',
installedAt: 0,
updatedAt: 0,
};
}
describe('PluginExampleDetail unavailable-state noun', () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
it('reads as "plugin" when a non-deck plugin ships no preview', async () => {
render(
<PluginExampleDetail
record={make({ id: 'example-live-artifact', title: 'Live Artifact' })}
onClose={() => {}}
onUse={() => {}}
/>,
);
const placeholder = await waitFor(() => screen.getByTestId('preview-unavailable'));
const copy = placeholder.textContent ?? '';
// The noun must match the card surface — calling a plugin a "skill" is
// what #3216 was filed for.
expect(copy).toMatch(/plugin/i);
expect(copy).not.toMatch(/\bskill\b/i);
});
it('reads as "template" when a deck-mode plugin ships no preview', async () => {
render(
<PluginExampleDetail
record={make({ id: 'replit-deck', title: 'Replit Deck', mode: 'deck' })}
onClose={() => {}}
onUse={() => {}}
/>,
);
const placeholder = await waitFor(() => screen.getByTestId('preview-unavailable'));
const copy = placeholder.textContent ?? '';
// Decks are surfaced as design templates in Home → Community, so the
// copy should track that vocabulary instead of saying "plugin" or
// "skill".
expect(copy).toMatch(/template/i);
expect(copy).not.toMatch(/\bskill\b/i);
});
});