open-design/apps/web/tests/components/DesignSystemsTab.test.tsx
elihahah666 80e685c656
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
landing-page-ci / Validate landing page (push) Failing after 2s
landing-page-deploy / Deploy landing page (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 1s
ci / Workspace unit tests (push) Failing after 1s
ci / Daemon workspace tests (push) Failing after 1s
ci / Web workspace tests (push) Failing after 1s
ci / Browser tests (push) Failing after 1s
ci / Build workspaces (push) Failing after 1s
ci / Validate workspace (push) Failing after 1s
ci / Runtime trace (push) Has been skipped
fix(web): polish design system source flows (#2933)
Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-25 12:38:05 +00:00

270 lines
9.3 KiB
TypeScript

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { DesignSystemSummary } from '@open-design/contracts';
import { DesignSystemsTab } from '../../src/components/DesignSystemsTab';
vi.mock('../../src/providers/registry', async () => {
const actual = await vi.importActual<typeof import('../../src/providers/registry')>(
'../../src/providers/registry',
);
return {
...actual,
fetchDesignSystemShowcase: vi.fn(async () => null),
updateDesignSystemDraft: vi.fn(async () => null),
deleteDesignSystemDraft: vi.fn(async () => true),
};
});
// DesignSystemCard lazy-loads its showcase iframe through an
// IntersectionObserver; an idle observer keeps thumbnails (and the registry
// fetch) out of the way so the tests only exercise filtering.
const originalIntersectionObserver = globalThis.IntersectionObserver;
class IdleIntersectionObserver {
observe() {}
disconnect() {}
unobserve() {}
}
beforeEach(() => {
globalThis.IntersectionObserver =
IdleIntersectionObserver as unknown as typeof IntersectionObserver;
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
globalThis.IntersectionObserver = originalIntersectionObserver;
});
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,
},
];
describe('DesignSystemsTab', () => {
it('surfaces user-created design systems in the gallery', () => {
render(
<DesignSystemsTab
systems={systems}
selectedId="user:acme"
onSelect={() => {}}
onPreview={() => {}}
onCreate={() => {}}
onOpenSystem={() => {}}
/>,
);
expect(screen.getByText('Create')).toBeTruthy();
expect(screen.getByText('Acme Design System')).toBeTruthy();
expect(screen.queryByText('Linear')).toBeNull();
openOfficialPresets();
expect(screen.getByText('Linear')).toBeTruthy();
});
it('routes create and open actions to the dedicated design-system flow', () => {
const onCreate = vi.fn();
const onOpenSystem = vi.fn();
render(
<DesignSystemsTab
systems={systems}
selectedId={null}
onSelect={() => {}}
onPreview={() => {}}
onCreate={onCreate}
onOpenSystem={onOpenSystem}
/>,
);
fireEvent.click(screen.getByText('Create'));
expect(onCreate).toHaveBeenCalledOnce();
fireEvent.click(screen.getByText('Edit'));
expect(onOpenSystem).toHaveBeenCalledWith('user:acme');
});
it('omits the built-in library Open button while keeping preview clicks', () => {
const onOpenSystem = vi.fn();
const onPreview = vi.fn();
render(
<DesignSystemsTab
systems={systems}
selectedId={null}
onSelect={() => {}}
onPreview={onPreview}
onCreate={() => {}}
onOpenSystem={onOpenSystem}
/>,
);
expect(screen.queryByRole('button', { name: 'Open' })).toBeNull();
openOfficialPresets();
fireEvent.click(screen.getByTestId('design-system-preview-linear'));
expect(onPreview).toHaveBeenCalledWith('linear');
expect(onOpenSystem).not.toHaveBeenCalledWith('linear');
});
});
// --- #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()}
onPreview={vi.fn()}
/>,
);
}
function openOfficialPresets() {
fireEvent.click(screen.getByRole('tab', { name: 'Official presets' }));
}
// 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');
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(screen.getByText('Retro Web One')).toBeTruthy();
expect(screen.getByText('Retro Web Two')).toBeTruthy();
// A web system from a different category must not leak back in.
expect(screen.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 grid 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');
});
});