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
445 lines
16 KiB
TypeScript
445 lines
16 KiB
TypeScript
// @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<typeof import('../../src/providers/registry')>(
|
|
'../../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(
|
|
<DesignSystemsTab
|
|
loading
|
|
systems={[]}
|
|
selectedId={null}
|
|
onSelect={() => {}}
|
|
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<ReturnType<typeof fetchDesignSystem>>) => void;
|
|
vi.mocked(fetchDesignSystem).mockImplementationOnce(
|
|
() => new Promise((resolve) => {
|
|
resolveDetail = resolve;
|
|
}),
|
|
);
|
|
const { container } = render(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId="user:acme"
|
|
onSelect={() => {}}
|
|
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(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId="user:acme"
|
|
onSelect={() => {}}
|
|
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(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId="user:acme"
|
|
onSelect={() => {}}
|
|
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(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId="user:acme"
|
|
onSelect={() => {}}
|
|
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(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId={null}
|
|
onSelect={() => {}}
|
|
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(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId={null}
|
|
onSelect={() => {}}
|
|
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(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId={null}
|
|
onSelect={onSelect}
|
|
onCreate={() => {}}
|
|
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<ReturnType<typeof updateDesignSystemDraft>>) => void;
|
|
vi.mocked(updateDesignSystemDraft).mockImplementationOnce(
|
|
() => new Promise((resolve) => {
|
|
resolveUpdate = resolve;
|
|
}),
|
|
);
|
|
render(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId={null}
|
|
onSelect={() => {}}
|
|
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(
|
|
<DesignSystemsTab
|
|
systems={systems}
|
|
selectedId={null}
|
|
onSelect={() => {}}
|
|
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<DesignSystemSummary> & Pick<DesignSystemSummary, 'id' | 'title'>,
|
|
): 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(
|
|
<DesignSystemsTab
|
|
systems={items}
|
|
selectedId={null}
|
|
onSelect={vi.fn()}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
// 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');
|
|
});
|
|
});
|