diff --git a/.tmp/proof/showcase-page.png b/.tmp/proof/showcase-page.png
new file mode 100644
index 0000000..317ea3a
Binary files /dev/null and b/.tmp/proof/showcase-page.png differ
diff --git a/packages/app/src/components/showcase/ShowcaseCard.test.tsx b/packages/app/src/components/showcase/ShowcaseCard.test.tsx
new file mode 100644
index 0000000..ea85477
--- /dev/null
+++ b/packages/app/src/components/showcase/ShowcaseCard.test.tsx
@@ -0,0 +1,59 @@
+import { screen } from '@testing-library/react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { ShowcaseCard } from './ShowcaseCard';
+import { ShowcaseItem, templateShowcaseLink } from './showcaseConfig';
+
+const baseItem: ShowcaseItem = {
+ id: 'template-mcp-server',
+ name: 'MCP Server',
+ description:
+ 'Scaffold a Model Context Protocol server that AI assistants connect to for domain-specific tools and context.',
+ category: 'templates',
+ link: templateShowcaseLink('mcp-server'),
+ linkLabel: 'Use template',
+ tags: ['recommended', 'ai', 'mcp', 'github'],
+ isRecommended: true,
+};
+
+describe('ShowcaseCard', () => {
+ it('renders name, description, category label and tags', async () => {
+ await renderInTestApp();
+
+ expect(screen.getByText('MCP Server')).toBeInTheDocument();
+ expect(screen.getByText(/Model Context Protocol server/)).toBeInTheDocument();
+ expect(screen.getByText('Code Templates')).toBeInTheDocument();
+ expect(screen.getByText('ai')).toBeInTheDocument();
+ expect(screen.getByText('mcp')).toBeInTheDocument();
+ });
+
+ it('shows the recommended badge only for recommended items', async () => {
+ const { rerender } = await renderInTestApp();
+ expect(screen.getByText('Recommended')).toBeInTheDocument();
+
+ rerender();
+ expect(screen.queryByText('Recommended')).not.toBeInTheDocument();
+ });
+
+ it('truncates tags to four', async () => {
+ const item: ShowcaseItem = {
+ ...baseItem,
+ id: 'many-tags',
+ tags: ['one', 'two', 'three', 'four', 'five', 'six'],
+ };
+ await renderInTestApp();
+
+ expect(screen.getByText('four')).toBeInTheDocument();
+ expect(screen.queryByText('five')).not.toBeInTheDocument();
+ expect(screen.queryByText('six')).not.toBeInTheDocument();
+ });
+
+ it('links the action button to the item link with its label', async () => {
+ await renderInTestApp();
+
+ const link = screen.getByRole('link', { name: 'Use template' });
+ expect(link).toHaveAttribute(
+ 'href',
+ '/self-service/templates/default/mcp-server',
+ );
+ });
+});
diff --git a/packages/app/src/components/showcase/ShowcasePage.test.tsx b/packages/app/src/components/showcase/ShowcasePage.test.tsx
new file mode 100644
index 0000000..eaae612
--- /dev/null
+++ b/packages/app/src/components/showcase/ShowcasePage.test.tsx
@@ -0,0 +1,73 @@
+import { screen, fireEvent } from '@testing-library/react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { ShowcasePage } from './ShowcasePage';
+import { showcaseItems, countByCategory } from './showcaseConfig';
+
+describe('ShowcasePage', () => {
+ it('renders every item with the total count and category chips', async () => {
+ await renderInTestApp();
+ const counts = countByCategory(showcaseItems);
+
+ expect(screen.getByText('Plugin Showcase')).toBeInTheDocument();
+ expect(
+ screen.getByText(`${counts.all} items available`),
+ ).toBeInTheDocument();
+ expect(screen.getByText(`All (${counts.all})`)).toBeInTheDocument();
+ expect(
+ screen.getByText(`Code Templates (${counts.templates})`),
+ ).toBeInTheDocument();
+ expect(screen.getByText(`GitHub (${counts.github})`)).toBeInTheDocument();
+ showcaseItems.forEach(item => {
+ expect(
+ document.querySelector(`[data-testid="showcase-card-${item.id}"]`),
+ ).not.toBeNull();
+ });
+ });
+
+ it('filters cards when a category chip is clicked', async () => {
+ await renderInTestApp();
+
+ fireEvent.click(screen.getByText(/^GitHub \(/));
+
+ const githubItems = showcaseItems.filter(i => i.category === 'github');
+ expect(
+ screen.getByText(`${githubItems.length} items available`),
+ ).toBeInTheDocument();
+ githubItems.forEach(item => {
+ expect(
+ document.querySelector(`[data-testid="showcase-card-${item.id}"]`),
+ ).not.toBeNull();
+ });
+ expect(
+ document.querySelector('[data-testid="showcase-card-template-mcp-server"]'),
+ ).toBeNull();
+ });
+
+ it('filters cards by search term', async () => {
+ await renderInTestApp();
+
+ fireEvent.change(screen.getByPlaceholderText('Search showcase...'), {
+ target: { value: 'mcp' },
+ });
+
+ expect(
+ document.querySelector('[data-testid="showcase-card-template-mcp-server"]'),
+ ).not.toBeNull();
+ expect(
+ document.querySelector(
+ '[data-testid="showcase-card-template-github-blank-repo"]',
+ ),
+ ).toBeNull();
+ });
+
+ it('shows the empty state when nothing matches', async () => {
+ await renderInTestApp();
+
+ fireEvent.change(screen.getByPlaceholderText('Search showcase...'), {
+ target: { value: 'zzzz-nothing' },
+ });
+
+ expect(screen.getByText(/No showcase items found/)).toBeInTheDocument();
+ expect(screen.getByText('0 items available')).toBeInTheDocument();
+ });
+});