// @vitest-environment jsdom import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { DesignSystemSummary } from '@open-design/contracts'; import { DesignSystemsTab } from '../../src/components/DesignSystemsTab'; import { fetchDesignSystem, updateDesignSystemDraft } from '../../src/providers/registry'; const exportMocks = vi.hoisted(() => ({ downloadDesignSystemArchive: vi.fn(async () => true), downloadProjectArchive: vi.fn(async () => false), })); vi.mock('../../src/providers/registry', async () => { const actual = await vi.importActual( '../../src/providers/registry', ); return { ...actual, fetchDesignSystem: vi.fn(async (id: string) => ({ id, title: id === 'linear' ? 'Linear' : 'Acme Design System', summary: id === 'linear' ? 'Quiet issue-tracker system.' : 'Internal product system.', category: id === 'linear' ? 'Productivity & SaaS' : 'Custom', body: `# ${id}\n\n## Colors\n- Primary #111111`, })), updateDesignSystemDraft: vi.fn(async () => null), deleteDesignSystemDraft: vi.fn(async () => true), }; }); vi.mock('../../src/runtime/exports', () => exportMocks); afterEach(() => { cleanup(); exportMocks.downloadDesignSystemArchive.mockReset(); exportMocks.downloadDesignSystemArchive.mockResolvedValue(true); exportMocks.downloadProjectArchive.mockReset(); exportMocks.downloadProjectArchive.mockResolvedValue(false); vi.restoreAllMocks(); }); const systems: DesignSystemSummary[] = [ { id: 'user:acme', title: 'Acme Design System', category: 'Custom', summary: 'Internal product system.', surface: 'web', source: 'user', status: 'draft', isEditable: true, updatedAt: '2026-05-13T03:19:00.000Z', }, { id: 'linear', title: 'Linear', category: 'Productivity & SaaS', summary: 'Quiet issue-tracker system.', surface: 'web', source: 'built-in', status: 'published', isEditable: false, }, ]; // The active scope's first row auto-selects into the detail pane, so a title // can appear twice (row + detail). Scope row lookups to the sidebar list. function list() { return within(screen.getByTestId('design-systems-list')); } function openOfficialPresets() { fireEvent.click(screen.getByRole('tab', { name: 'Official presets' })); } describe('DesignSystemsTab', () => { it('renders structured list and preview skeletons while design systems load', () => { const { container } = render( {}} onCreate={() => {}} onOpenSystem={() => {}} />, ); expect(screen.getByTestId('design-systems-sidebar-skeleton')).toBeTruthy(); expect(screen.getByTestId('design-systems-preview-skeleton')).toBeTruthy(); expect(screen.getByTestId('design-systems-loading-row-0')).toBeTruthy(); expect(screen.getByText('Loading design systems…')).toBeTruthy(); expect(container.querySelector('.loading-spinner')).toBeNull(); }); it('keeps the summary-derived kit visible while the selected system detail resolves', async () => { let resolveDetail!: (value: Awaited>) => void; vi.mocked(fetchDesignSystem).mockImplementationOnce( () => new Promise((resolve) => { resolveDetail = resolve; }), ); const { container } = render( {}} onCreate={() => {}} onOpenSystem={() => {}} />, ); expect(screen.getByTestId('design-kit-view-user:acme')).toBeTruthy(); expect(screen.queryByTestId('design-system-detail-loading-user:acme')).toBeNull(); expect(container.querySelector('.loading-spinner')).toBeNull(); resolveDetail({ id: 'user:acme', title: 'Acme Design System', summary: 'Internal product system.', category: 'Custom', body: '# Acme\n\n## Colors\n- Primary #111111', }); await screen.findByTestId('design-kit-view-user:acme'); }); it('uses design-system scopes directly instead of a design-system/template switcher', () => { render( {}} onCreate={() => {}} onOpenSystem={() => {}} />, ); expect(screen.queryByRole('tab', { name: 'Design system' })).toBeNull(); expect(screen.queryByRole('tab', { name: 'Template' })).toBeNull(); expect(screen.getByRole('tab', { name: 'Your systems' }).textContent).toContain('1'); expect(screen.getByRole('tab', { name: 'Official presets' }).textContent).toContain('1'); expect(screen.getByRole('tab', { name: 'Enterprise' }).textContent).toContain('Coming soon'); }); it('separates user-created design systems from the official preset library', () => { render( {}} onCreate={() => {}} onOpenSystem={() => {}} />, ); // "Your systems" is the default scope: Acme shows, Linear (a preset) does not. expect(screen.getByTestId('design-systems-create').textContent).toContain('Create'); expect(screen.getByTestId('design-system-card-user:acme')).toBeTruthy(); expect(screen.queryByTestId('design-system-card-linear')).toBeNull(); openOfficialPresets(); expect(screen.getByTestId('design-system-card-linear')).toBeTruthy(); expect(list().queryByText('Acme Design System')).toBeNull(); }); it('shows the user system scenario (summary) as the row subtitle, not a generic placeholder', () => { render( {}} onCreate={() => {}} onOpenSystem={() => {}} />, ); // The user row's subtitle now reads the scenario (summary) instead of the // repeated "Design system" placeholder it used to show. const row = within(screen.getByTestId('design-system-card-user:acme')); expect(row.getByText('Internal product system.')).toBeTruthy(); expect(row.queryByText('Design system')).toBeNull(); }); it('routes create and edit actions to the dedicated design-system flow', async () => { const onCreate = vi.fn(); const onOpenSystem = vi.fn(); render( {}} onCreate={onCreate} onOpenSystem={onOpenSystem} />, ); fireEvent.click(screen.getByTestId('design-systems-create')); expect(onCreate).toHaveBeenCalledOnce(); // Acme is the only user system, so it auto-selects into the detail pane, // exposing the agent edit action that routes back into the authoring flow. fireEvent.click(await screen.findByRole('button', { name: /Edit with agent/i })); expect(onOpenSystem).toHaveBeenCalledWith('user:acme'); }); it('keeps built-in library systems read-only with the redundant cover removed', async () => { const onOpenSystem = vi.fn(); render( {}} onCreate={() => {}} onOpenSystem={onOpenSystem} />, ); openOfficialPresets(); // Linear auto-selects into the read-only detail pane. await screen.findByTestId('design-kit-view-linear'); // A built-in preset is browse-only: no agent edit affordance, and the // redundant top showcase cover (with its preview button) has been removed. expect(screen.queryByRole('button', { name: /Edit with agent/i })).toBeNull(); expect(screen.queryByTestId('design-kit-cover-preview')).toBeNull(); expect(onOpenSystem).not.toHaveBeenCalled(); }); it('sets a system as the global default through the detail pane', async () => { const onSelect = vi.fn(); render( {}} onOpenSystem={() => {}} />, ); openOfficialPresets(); // "Make default" now lives in the detail's ⋯ overflow menu. fireEvent.click(await screen.findByTestId('design-kit-more-actions')); fireEvent.click(screen.getByRole('menuitem', { name: 'Default for new chats' })); expect(onSelect).toHaveBeenCalledWith('linear'); }); it('shows loading and result feedback when publishing a user system', async () => { let resolveUpdate!: (value: Awaited>) => void; vi.mocked(updateDesignSystemDraft).mockImplementationOnce( () => new Promise((resolve) => { resolveUpdate = resolve; }), ); render( {}} onCreate={() => {}} onOpenSystem={() => {}} />, ); const toggle = await screen.findByRole('button', { name: 'Draft' }); fireEvent.click(toggle); expect(toggle.getAttribute('aria-busy')).toBe('true'); expect(screen.getByText('Loading…')).toBeTruthy(); resolveUpdate({ id: 'user:acme', title: 'Acme Design System', summary: 'Internal product system.', category: 'Custom', status: 'published', body: '# Acme', }); await waitFor(() => expect(screen.getByText('Done')).toBeTruthy()); }); it('shows loading and result feedback for detail overflow downloads', async () => { let resolveDownload!: (value: boolean) => void; exportMocks.downloadDesignSystemArchive.mockImplementationOnce( () => new Promise((resolve) => { resolveDownload = resolve; }), ); render( {}} onCreate={() => {}} onOpenSystem={() => {}} />, ); fireEvent.click(await screen.findByTestId('design-kit-more-actions')); fireEvent.click(screen.getByRole('menuitem', { name: 'Download design system (.zip + SKILLS.md)' })); expect(screen.getByText('Download design system (.zip + SKILLS.md)')).toBeTruthy(); resolveDownload(true); await waitFor(() => expect(screen.getByText('Done')).toBeTruthy()); }); }); // --- #2062: built-in library surface-chip filtering ----------------------- function ds( overrides: Partial & Pick, ): DesignSystemSummary { return { id: overrides.id, title: overrides.title, category: overrides.category ?? 'Uncategorized', summary: overrides.summary ?? `${overrides.title} summary`, surface: overrides.surface ?? 'web', }; } // Two style categories, each spanning more than one surface, so a style // filter genuinely narrows every surface count. None set `source`/`isEditable`, // so they populate the built-in library section the surface chips belong to. // Retro: web x2, image x1 Social: web x1, image x1 const librarySystems: DesignSystemSummary[] = [ ds({ id: 'retro-web-1', title: 'Retro Web One', category: 'Retro', surface: 'web' }), ds({ id: 'retro-web-2', title: 'Retro Web Two', category: 'Retro', surface: 'web' }), ds({ id: 'retro-img-1', title: 'Retro Image One', category: 'Retro', surface: 'image' }), ds({ id: 'social-web-1', title: 'Social Web One', category: 'Social', surface: 'web' }), ds({ id: 'social-img-1', title: 'Social Image One', category: 'Social', surface: 'image' }), ]; function renderTab(items: DesignSystemSummary[] = librarySystems) { return render( , ); } // The surface pill renders its label and a `.filter-pill-count` span; read // the count back by the visible label so assertions describe the UI. function surfacePillCount(label: string): string | null { for (const pill of screen.getAllByRole('tab')) { const countEl = pill.querySelector('.filter-pill-count'); if (!countEl) continue; const labelText = (pill.textContent ?? '').replace(countEl.textContent ?? '', ''); if (labelText === label) return countEl.textContent ?? null; } return null; } function selectCategory(value: string) { fireEvent.change(screen.getByTestId('design-systems-category-select'), { target: { value }, }); } describe('DesignSystemsTab surface filtering', () => { it('scopes surface pill counts to the selected style category', () => { // Regression: nexu-io/open-design#2062 — surface chips kept showing the // unfiltered totals after a style category was applied. The counts must // describe the filtered result set, otherwise "All 149 / Web 149" is a // lie about what the user is looking at. renderTab(); openOfficialPresets(); expect(surfacePillCount('All')).toBe('5'); expect(surfacePillCount('Web')).toBe('3'); expect(surfacePillCount('Image')).toBe('2'); selectCategory('Retro'); expect(surfacePillCount('All')).toBe('3'); expect(surfacePillCount('Web')).toBe('2'); expect(surfacePillCount('Image')).toBe('1'); }); it('keeps the style category when a surface chip refines within it', () => { // Regression: nexu-io/open-design#2062 — clicking a surface chip reset // the style category to "All", discarding the user's filter instead of // refining inside it. The category survives when it still has matches // for the chosen surface. renderTab(); openOfficialPresets(); selectCategory('Retro'); fireEvent.click(screen.getByRole('tab', { name: /^Web/ })); expect( (screen.getByTestId('design-systems-category-select') as HTMLSelectElement).value, ).toBe('Retro'); expect(list().getByText('Retro Web One')).toBeTruthy(); expect(list().getByText('Retro Web Two')).toBeTruthy(); // A web system from a different category must not leak back in. expect(list().queryByText('Social Web One')).toBeNull(); }); it('hides a surface chip that has no systems in the selected style category', () => { // Consequence of the #2062 fix: a chip whose count drops to zero for the // active style category falls away, the same way a globally-empty // surface already does — so a chip never advertises an empty result set. const webOnlyCategory: DesignSystemSummary[] = [ ds({ id: 'tools-web-1', title: 'Tools Web One', category: 'Tools', surface: 'web' }), ds({ id: 'retro-web-1', title: 'Retro Web One', category: 'Retro', surface: 'web' }), ds({ id: 'retro-img-1', title: 'Retro Image One', category: 'Retro', surface: 'image' }), ]; renderTab(webOnlyCategory); openOfficialPresets(); expect(screen.queryByRole('tab', { name: /^Image/ })).not.toBeNull(); selectCategory('Tools'); // Tools has only web systems, so the Image chip no longer applies. expect(screen.queryByRole('tab', { name: /^Image/ })).toBeNull(); expect(surfacePillCount('Web')).toBe('1'); }); it('keeps the active surface chip visible when a search filters out all of its results', () => { // PR #2141 review (Looper): the scoped-count hide rule must never remove // the chip the user is currently on. Select Image, then search for text // only web systems match — the Image chip must stay, and stay selected, // so the active filter is visible instead of an empty list with no chip. renderTab(); openOfficialPresets(); fireEvent.click(screen.getByRole('tab', { name: /^Image/ })); fireEvent.change(screen.getByTestId('design-systems-search'), { target: { value: 'Web' }, }); const imageTab = screen.queryByRole('tab', { name: /^Image/ }); expect(imageTab).not.toBeNull(); expect(imageTab?.getAttribute('aria-selected')).toBe('true'); // ...and it honestly reports zero matches for the current search. expect(surfacePillCount('Image')).toBe('0'); }); });