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(); + }); +});