mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-07 23:09:34 +08:00
561 lines
20 KiB
TypeScript
561 lines
20 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
BROWSER_USE_ACTION_TOTAL,
|
|
BROWSER_USE_CATEGORIES,
|
|
REFERENCE_GROUPS,
|
|
REFERENCE_TOTAL,
|
|
browserUseActionById,
|
|
browserUsePrompt,
|
|
browserFileName,
|
|
faviconUrl,
|
|
filterBrowserUseCategories,
|
|
filterReferenceGroups,
|
|
formatAddressDisplay,
|
|
formatAddressDisplayParts,
|
|
hostnameFromUrl,
|
|
isHistoryEntry,
|
|
isHistoryUrl,
|
|
labelFromUrl,
|
|
loadHistory,
|
|
normalizeBrowserAddress,
|
|
pageBriefMarkdown,
|
|
referenceIconUrl,
|
|
sameUrl,
|
|
saveHistory,
|
|
} from '../../src/components/DesignBrowserPanel';
|
|
import {
|
|
browserCommentFilePath,
|
|
isProjectHtmlBrowserUrl,
|
|
projectRelativePathFromBrowserUrl,
|
|
} from '../../src/components/design-browser-tools';
|
|
|
|
describe('normalizeBrowserAddress', () => {
|
|
it('passes through absolute http URLs unchanged', () => {
|
|
expect(normalizeBrowserAddress('http://example.com/page')).toBe('http://example.com/page');
|
|
});
|
|
|
|
it('passes through absolute https URLs unchanged', () => {
|
|
expect(normalizeBrowserAddress('https://example.com/page')).toBe('https://example.com/page');
|
|
});
|
|
|
|
it('passes through file URLs unchanged', () => {
|
|
expect(normalizeBrowserAddress('file:///Users/me/page.html')).toBe('file:///Users/me/page.html');
|
|
});
|
|
|
|
it('trims surrounding whitespace before matching', () => {
|
|
expect(normalizeBrowserAddress(' https://example.com ')).toBe('https://example.com');
|
|
});
|
|
|
|
it('promotes a bare domain to https', () => {
|
|
expect(normalizeBrowserAddress('example.com')).toBe('https://example.com');
|
|
});
|
|
|
|
it('promotes a bare domain with a path and port to https', () => {
|
|
expect(normalizeBrowserAddress('example.com:8080/path')).toBe('https://example.com:8080/path');
|
|
});
|
|
|
|
it('maps localhost to http', () => {
|
|
expect(normalizeBrowserAddress('localhost')).toBe('http://localhost');
|
|
expect(normalizeBrowserAddress('localhost:3000/dash')).toBe('http://localhost:3000/dash');
|
|
});
|
|
|
|
it('maps loopback IPs to http', () => {
|
|
expect(normalizeBrowserAddress('127.0.0.1')).toBe('http://127.0.0.1');
|
|
expect(normalizeBrowserAddress('127.0.0.1:5173')).toBe('http://127.0.0.1:5173');
|
|
expect(normalizeBrowserAddress('0.0.0.0:8000')).toBe('http://0.0.0.0:8000');
|
|
});
|
|
|
|
it('resolves /api, /artifacts, /frames paths against the page origin', () => {
|
|
const origin = window.location.origin;
|
|
expect(normalizeBrowserAddress('/api/runs')).toBe(`${origin}/api/runs`);
|
|
expect(normalizeBrowserAddress('/artifacts/x.png')).toBe(`${origin}/artifacts/x.png`);
|
|
expect(normalizeBrowserAddress('/frames/1')).toBe(`${origin}/frames/1`);
|
|
});
|
|
|
|
it('maps other absolute paths to file URLs', () => {
|
|
expect(normalizeBrowserAddress('/Users/me/page.html')).toBe('file:///Users/me/page.html');
|
|
expect(normalizeBrowserAddress('/some path/with space')).toBe(`file://${encodeURI('/some path/with space')}`);
|
|
});
|
|
|
|
it('treats free text as a Google search', () => {
|
|
expect(normalizeBrowserAddress('design inspiration')).toBe(
|
|
'https://www.google.com/search?q=design%20inspiration',
|
|
);
|
|
});
|
|
|
|
it('maps an empty string to about:blank', () => {
|
|
expect(normalizeBrowserAddress('')).toBe('about:blank');
|
|
expect(normalizeBrowserAddress(' ')).toBe('about:blank');
|
|
});
|
|
|
|
it('passes through an explicit about:blank', () => {
|
|
expect(normalizeBrowserAddress('about:blank')).toBe('about:blank');
|
|
});
|
|
});
|
|
|
|
describe('inspiration action prompts', () => {
|
|
it('keeps the inspiration catalogue at the intended 43 actions', () => {
|
|
expect(BROWSER_USE_ACTION_TOTAL).toBe(43);
|
|
expect(BROWSER_USE_CATEGORIES.map((category) => category.id)).toEqual([
|
|
'assets',
|
|
'tokens',
|
|
'motion',
|
|
'visual',
|
|
'structure',
|
|
'project',
|
|
'general',
|
|
]);
|
|
expect(BROWSER_USE_CATEGORIES[0]?.actions.slice(0, 3).map((action) => action.id)).toEqual([
|
|
'extract_logo',
|
|
'list_images',
|
|
'download_assets',
|
|
]);
|
|
expect(BROWSER_USE_CATEGORIES[BROWSER_USE_CATEGORIES.length - 1]?.actions.map((action) => action.id)).toContain('navigate');
|
|
expect(browserUseActionById('extract_fonts')?.output).toContain('typography.json');
|
|
expect(browserUseActionById('extract_og_metadata')?.output).toContain('OG/Twitter');
|
|
expect(browserUseActionById('audit_accessibility')?.output).toContain('A11y');
|
|
});
|
|
|
|
it('filters inspiration actions by action evidence and category title', () => {
|
|
const visualCategory = BROWSER_USE_CATEGORIES.find((category) => category.id === 'visual');
|
|
const categoryTitle = (category: (typeof BROWSER_USE_CATEGORIES)[number]) =>
|
|
({
|
|
assets: '素材提取',
|
|
tokens: '设计语言',
|
|
motion: '动效',
|
|
visual: '视觉校验',
|
|
structure: '组件结构',
|
|
project: '项目运行',
|
|
general: '通用操作',
|
|
})[category.id] ?? category.title;
|
|
expect(visualCategory).toBeTruthy();
|
|
expect(filterBrowserUseCategories(BROWSER_USE_CATEGORIES, 'a11y', categoryTitle)).toEqual([
|
|
{
|
|
...visualCategory!,
|
|
actions: [browserUseActionById('audit_accessibility')!],
|
|
},
|
|
]);
|
|
expect(
|
|
filterBrowserUseCategories(
|
|
BROWSER_USE_CATEGORIES,
|
|
'字体',
|
|
categoryTitle,
|
|
(action) => (action.id === 'extract_fonts' ? ['字体家族、字号、字重和 @font-face'] : []),
|
|
)[0]?.actions,
|
|
).toEqual([browserUseActionById('extract_fonts')!]);
|
|
expect(filterBrowserUseCategories(BROWSER_USE_CATEGORIES, '通用操作', categoryTitle)[0]?.id).toBe('general');
|
|
expect(filterBrowserUseCategories(BROWSER_USE_CATEGORIES, 'no-such-action', categoryTitle)).toEqual([]);
|
|
});
|
|
|
|
it('builds an agent-browser prompt bound to the current browser tab context', () => {
|
|
const action = browserUseActionById('extract_colors');
|
|
expect(action).not.toBeNull();
|
|
|
|
const prompt = browserUsePrompt(action!, {
|
|
browserFilePath: 'browser:https://example.com',
|
|
projectId: 'proj-1',
|
|
resolvedDir: '/tmp/open-design/project',
|
|
tabLabel: 'Example landing',
|
|
title: 'Example',
|
|
url: 'https://example.com',
|
|
});
|
|
|
|
expect(prompt).toContain('@agent-browser');
|
|
expect(prompt).toContain('Use the selected Open Design Browser tab as the bound target.');
|
|
expect(prompt).toContain('- tab: Example landing');
|
|
expect(prompt).toContain('- url: https://example.com');
|
|
expect(prompt).toContain('Operation: extract_colors');
|
|
expect(prompt).toContain('browser-use / browser-harness style evidence');
|
|
});
|
|
});
|
|
|
|
describe('browser tool file targeting', () => {
|
|
it('treats localhost dev-server pages as browser targets, not single-file HTML saves', () => {
|
|
const url = 'http://localhost:3000/src/App.jsx';
|
|
|
|
expect(browserCommentFilePath(url, '/Users/me/project')).toBe(`browser:${url}`);
|
|
expect(projectRelativePathFromBrowserUrl(url, '/Users/me/project')).toBeNull();
|
|
expect(isProjectHtmlBrowserUrl(url, '/Users/me/project')).toBe(false);
|
|
});
|
|
|
|
it('maps project-local file HTML pages back to editable project files', () => {
|
|
const url = 'file:///Users/me/project/dist/index.html';
|
|
|
|
expect(browserCommentFilePath(url, '/Users/me/project')).toBe('dist/index.html');
|
|
expect(projectRelativePathFromBrowserUrl(url, '/Users/me/project')).toBe('dist/index.html');
|
|
expect(isProjectHtmlBrowserUrl(url, '/Users/me/project')).toBe(true);
|
|
});
|
|
|
|
it('keeps project-local non-HTML files as commentable browser targets without save affordance', () => {
|
|
const url = 'file:///Users/me/project/src/App.jsx';
|
|
|
|
expect(browserCommentFilePath(url, '/Users/me/project')).toBe('src/App.jsx');
|
|
expect(projectRelativePathFromBrowserUrl(url, '/Users/me/project')).toBe('src/App.jsx');
|
|
expect(isProjectHtmlBrowserUrl(url, '/Users/me/project')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('sameUrl', () => {
|
|
it('treats trailing slashes as equivalent', () => {
|
|
expect(sameUrl('https://example.com', 'https://example.com/')).toBe(true);
|
|
expect(sameUrl('https://example.com///', 'https://example.com')).toBe(true);
|
|
});
|
|
|
|
it('distinguishes different paths', () => {
|
|
expect(sameUrl('https://example.com/a', 'https://example.com/b')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('labelFromUrl', () => {
|
|
it('returns New Tab for the blank URL', () => {
|
|
expect(labelFromUrl('about:blank')).toBe('New Tab');
|
|
});
|
|
|
|
it('strips the www. prefix from the host', () => {
|
|
expect(labelFromUrl('https://www.example.com/page')).toBe('example.com');
|
|
expect(labelFromUrl('https://sub.example.com/')).toBe('sub.example.com');
|
|
});
|
|
|
|
it('falls back to the raw value when the URL cannot be parsed', () => {
|
|
expect(labelFromUrl('not a url')).toBe('not a url');
|
|
});
|
|
});
|
|
|
|
describe('formatAddressDisplay', () => {
|
|
it('keeps the URL alone when the title is only the host fallback', () => {
|
|
expect(formatAddressDisplay('https://www.example.com/path', 'example.com')).toBe('https://www.example.com/path');
|
|
});
|
|
|
|
it('appends a real page title for the passive address display', () => {
|
|
expect(formatAddressDisplay('https://www.baidu.com/', '百度一下,你就知道')).toBe(
|
|
'https://www.baidu.com / 百度一下,你就知道',
|
|
);
|
|
});
|
|
|
|
it('exposes URL and title as separate passive display parts', () => {
|
|
expect(formatAddressDisplayParts('https://brandfetch.com/', 'Just a moment...')).toEqual({
|
|
url: 'https://brandfetch.com',
|
|
title: 'Just a moment...',
|
|
});
|
|
});
|
|
|
|
it('keeps the blank tab display empty', () => {
|
|
expect(formatAddressDisplay('about:blank', 'New Tab')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('hostnameFromUrl', () => {
|
|
it('returns a compact hostname without www', () => {
|
|
expect(hostnameFromUrl('https://www.example.com/docs')).toBe('example.com');
|
|
});
|
|
|
|
it('falls back to the raw value when parsing fails', () => {
|
|
expect(hostnameFromUrl('not a url')).toBe('not a url');
|
|
});
|
|
});
|
|
|
|
describe('faviconUrl', () => {
|
|
it('derives a same-origin favicon URL for http pages', () => {
|
|
expect(faviconUrl('https://www.example.com/docs')).toBe('https://www.example.com/favicon.ico');
|
|
});
|
|
|
|
it('skips non-http urls', () => {
|
|
expect(faviconUrl('file:///Users/me/page.html')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('referenceIconUrl', () => {
|
|
it('routes a reference site through the favicon service with its hostname', () => {
|
|
expect(referenceIconUrl('https://dribbble.com/')).toBe(
|
|
'https://www.google.com/s2/favicons?sz=64&domain=dribbble.com',
|
|
);
|
|
});
|
|
|
|
it('preserves subdomains and accepts a custom size', () => {
|
|
expect(referenceIconUrl('https://styles.refero.design/', 32)).toBe(
|
|
'https://www.google.com/s2/favicons?sz=32&domain=styles.refero.design',
|
|
);
|
|
});
|
|
|
|
it('returns a resolvable icon for every catalogued reference site', () => {
|
|
for (const group of REFERENCE_GROUPS) {
|
|
for (const site of group.sites) {
|
|
expect(referenceIconUrl(site.url)).toMatch(
|
|
/^https:\/\/www\.google\.com\/s2\/favicons\?sz=64&domain=/,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('skips non-http urls so the globe fallback still applies', () => {
|
|
expect(referenceIconUrl('file:///Users/me/page.html')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('isHistoryUrl', () => {
|
|
it('accepts http(s) and file URLs', () => {
|
|
expect(isHistoryUrl('https://example.com')).toBe(true);
|
|
expect(isHistoryUrl('http://localhost:3000')).toBe(true);
|
|
expect(isHistoryUrl('file:///Users/me/x.html')).toBe(true);
|
|
});
|
|
|
|
it('rejects the blank URL', () => {
|
|
expect(isHistoryUrl('about:blank')).toBe(false);
|
|
});
|
|
|
|
it('rejects non http/file schemes', () => {
|
|
expect(isHistoryUrl('data:text/html,hi')).toBe(false);
|
|
expect(isHistoryUrl('mailto:hi@example.com')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('pageBriefMarkdown', () => {
|
|
it('renders title, source, and populated sections while skipping empty ones', () => {
|
|
const md = pageBriefMarkdown(
|
|
{
|
|
title: 'Example',
|
|
url: 'https://example.com',
|
|
description: 'A description',
|
|
headings: ['Hero', ' ', 'Features'],
|
|
images: [],
|
|
links: [{ text: 'Docs', url: 'https://example.com/docs' }],
|
|
colors: [{ value: 'rgb(0, 0, 0)', count: 4 }],
|
|
},
|
|
'https://fallback.example.com',
|
|
);
|
|
expect(md).toContain('# Example');
|
|
expect(md).toContain('Source: https://example.com');
|
|
expect(md).toContain('## Description');
|
|
expect(md).toContain('## Headings');
|
|
expect(md).toContain('- Hero');
|
|
expect(md).toContain('- Features');
|
|
expect(md).not.toContain('## Images');
|
|
expect(md).toContain('## Links');
|
|
expect(md).toContain('- Docs - https://example.com/docs');
|
|
expect(md).toContain('## Colors');
|
|
expect(md).toContain('- rgb(0, 0, 0) (4)');
|
|
expect(md).not.toContain('Browser Harness');
|
|
});
|
|
|
|
it('falls back to label and url when the brief omits them', () => {
|
|
const md = pageBriefMarkdown({}, 'https://www.fallback.example.com/path');
|
|
expect(md).toContain('# fallback.example.com');
|
|
expect(md).toContain('Source: https://www.fallback.example.com/path');
|
|
});
|
|
});
|
|
|
|
describe('browserFileName', () => {
|
|
it('sanitizes the host and includes the prefix and extension', () => {
|
|
const name = browserFileName('browser-capture', 'https://www.example.com/page', 'png');
|
|
expect(name).toMatch(/^browser\/browser-capture-example\.com-[\dTZ-]+\.png$/);
|
|
});
|
|
|
|
it('uses a page fallback when the host sanitizes to empty', () => {
|
|
const name = browserFileName('browser-brief', 'about:blank', 'md');
|
|
// about:blank -> labelFromUrl 'New Tab' -> 'New-Tab'
|
|
expect(name).toMatch(/^browser\/browser-brief-New-Tab-[\dTZ-]+\.md$/);
|
|
});
|
|
});
|
|
|
|
describe('isHistoryEntry', () => {
|
|
it('accepts a well-formed entry', () => {
|
|
expect(
|
|
isHistoryEntry({ url: 'https://x', title: 'X', iconUrl: 'https://x/favicon.ico', lastVisitedAt: 1, visitCount: 1 }),
|
|
).toBe(true);
|
|
});
|
|
|
|
it('rejects malformed values', () => {
|
|
expect(isHistoryEntry(null)).toBe(false);
|
|
expect(isHistoryEntry([])).toBe(false);
|
|
expect(isHistoryEntry('x')).toBe(false);
|
|
expect(isHistoryEntry({ url: 1, title: 'X', lastVisitedAt: 1, visitCount: 1 })).toBe(false);
|
|
expect(isHistoryEntry({ url: 'x', title: 'X', lastVisitedAt: 1 })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('loadHistory / saveHistory round-trip', () => {
|
|
const projectId = 'proj-history';
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
window.localStorage.clear();
|
|
});
|
|
|
|
it('returns an empty array when nothing is stored', () => {
|
|
expect(loadHistory(projectId)).toEqual([]);
|
|
});
|
|
|
|
it('round-trips entries and sorts by lastVisitedAt descending', () => {
|
|
saveHistory(projectId, [
|
|
{ url: 'https://a.com', title: 'A', lastVisitedAt: 100, visitCount: 1 },
|
|
{ url: 'https://b.com', title: 'B', lastVisitedAt: 300, visitCount: 2 },
|
|
{ url: 'https://c.com', title: 'C', lastVisitedAt: 200, visitCount: 1 },
|
|
]);
|
|
const loaded = loadHistory(projectId);
|
|
expect(loaded.map((entry) => entry.url)).toEqual([
|
|
'https://b.com',
|
|
'https://c.com',
|
|
'https://a.com',
|
|
]);
|
|
});
|
|
|
|
it('drops malformed entries on load', () => {
|
|
window.localStorage.setItem(
|
|
`od:design-browser:${projectId}:history:v1`,
|
|
JSON.stringify([
|
|
{ url: 'https://ok.com', title: 'OK', lastVisitedAt: 1, visitCount: 1 },
|
|
{ url: 123, title: 'bad', lastVisitedAt: 1, visitCount: 1 },
|
|
]),
|
|
);
|
|
const loaded = loadHistory(projectId);
|
|
expect(loaded).toHaveLength(1);
|
|
expect(loaded[0]?.url).toBe('https://ok.com');
|
|
});
|
|
|
|
it('returns an empty array for corrupt or non-array JSON', () => {
|
|
const key = `od:design-browser:${projectId}:history:v1`;
|
|
window.localStorage.setItem(key, 'not json');
|
|
expect(loadHistory(projectId)).toEqual([]);
|
|
window.localStorage.setItem(key, JSON.stringify({ not: 'an array' }));
|
|
expect(loadHistory(projectId)).toEqual([]);
|
|
});
|
|
|
|
it('caps stored history at the HISTORY_LIMIT on save and load', () => {
|
|
const many = Array.from({ length: 120 }, (_, index) => ({
|
|
url: `https://site-${index}.com`,
|
|
title: `Site ${index}`,
|
|
lastVisitedAt: index,
|
|
visitCount: 1,
|
|
}));
|
|
saveHistory(projectId, many);
|
|
expect(loadHistory(projectId)).toHaveLength(80);
|
|
});
|
|
});
|
|
|
|
describe('REFERENCE_GROUPS catalogue', () => {
|
|
it('exposes every documented designer reference category', () => {
|
|
const ids = REFERENCE_GROUPS.map((group) => group.id);
|
|
expect(ids).toEqual([
|
|
'inspiration',
|
|
'interfaces',
|
|
'motion',
|
|
'color',
|
|
'type',
|
|
'icons',
|
|
'illustration',
|
|
'photography',
|
|
'3d',
|
|
'mockups',
|
|
'systems',
|
|
'components',
|
|
'guidelines',
|
|
'tools',
|
|
]);
|
|
});
|
|
|
|
it('gives every group a title and at least one absolute https/http site', () => {
|
|
for (const group of REFERENCE_GROUPS) {
|
|
expect(group.title.length).toBeGreaterThan(0);
|
|
expect(group.sites.length).toBeGreaterThan(0);
|
|
for (const site of group.sites) {
|
|
expect(site.label.length).toBeGreaterThan(0);
|
|
expect(site.detail.length).toBeGreaterThan(0);
|
|
expect(site.url).toMatch(/^https?:\/\//);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('uses unique category ids and unique site urls', () => {
|
|
const ids = REFERENCE_GROUPS.map((group) => group.id);
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
const urls = REFERENCE_GROUPS.flatMap((group) => group.sites.map((site) => site.url));
|
|
expect(new Set(urls).size).toBe(urls.length);
|
|
});
|
|
|
|
it('keeps REFERENCE_TOTAL in sync with the catalogue', () => {
|
|
const counted = REFERENCE_GROUPS.reduce((sum, group) => sum + group.sites.length, 0);
|
|
expect(REFERENCE_TOTAL).toBe(counted);
|
|
});
|
|
|
|
it('includes the handoff and component-library reference URLs', () => {
|
|
const urls = new Set(REFERENCE_GROUPS.flatMap((group) => group.sites.map((site) => site.url)));
|
|
expect(Array.from(urls)).toEqual(expect.arrayContaining([
|
|
'https://thesvg.org/',
|
|
'https://unsplash.com/',
|
|
'https://motionsites.ai/',
|
|
'https://motion.page/showcase/',
|
|
'https://styles.refero.design/',
|
|
'https://brandfetch.com/',
|
|
'https://gsap.com/',
|
|
'https://transitions.dev/',
|
|
'https://fonts.google.com/',
|
|
'https://animography.net/',
|
|
'https://reactbits.dev/text-animations/shiny-text',
|
|
'https://toolfolio.io/',
|
|
'https://www.whirrls.com/',
|
|
'https://startups.gallery/',
|
|
'https://www.worldindots.com/',
|
|
'https://getdesign.md/',
|
|
'https://github.com/superset-sh/superset',
|
|
'https://svglogos.dev/',
|
|
'https://icons.lobehub.com/',
|
|
'https://animations.dev/',
|
|
'https://impeccable.style/',
|
|
'https://www.tasteskill.dev/',
|
|
'https://base-ui.com/',
|
|
'https://ui.shadcn.com/',
|
|
'https://www.heroui.com/',
|
|
]));
|
|
});
|
|
});
|
|
|
|
describe('filterReferenceGroups', () => {
|
|
it('returns every group untouched for the "all" category and an empty query', () => {
|
|
const result = filterReferenceGroups(REFERENCE_GROUPS, 'all', '');
|
|
expect(result.map((group) => group.id)).toEqual(REFERENCE_GROUPS.map((group) => group.id));
|
|
expect(result).toEqual(REFERENCE_GROUPS);
|
|
});
|
|
|
|
it('narrows to a single group when a category id is active', () => {
|
|
const result = filterReferenceGroups(REFERENCE_GROUPS, 'motion', '');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.id).toBe('motion');
|
|
expect(result[0]?.sites.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('matches sites by label, hostname, or detail across all categories', () => {
|
|
const byLabel = filterReferenceGroups(REFERENCE_GROUPS, 'all', 'dribbble');
|
|
expect(byLabel.flatMap((group) => group.sites.map((site) => site.label))).toContain('Dribbble');
|
|
|
|
const byHostname = filterReferenceGroups(REFERENCE_GROUPS, 'all', 'unsplash.com');
|
|
expect(byHostname.flatMap((group) => group.sites.map((site) => site.label))).toContain('Unsplash');
|
|
|
|
const byDetail = filterReferenceGroups(REFERENCE_GROUPS, 'all', 'contrast');
|
|
expect(byDetail.flatMap((group) => group.sites.map((site) => site.label))).toContain('WebAIM Contrast');
|
|
});
|
|
|
|
it('keeps an entire group when the query matches its title', () => {
|
|
const color = REFERENCE_GROUPS.find((group) => group.id === 'color');
|
|
const result = filterReferenceGroups(REFERENCE_GROUPS, 'all', 'color');
|
|
const matchedColor = result.find((group) => group.id === 'color');
|
|
expect(matchedColor?.sites).toEqual(color?.sites);
|
|
});
|
|
|
|
it('drops groups with no surviving sites and is case-insensitive', () => {
|
|
const result = filterReferenceGroups(REFERENCE_GROUPS, 'all', 'COOLORS');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.id).toBe('color');
|
|
expect(result[0]?.sites.map((site) => site.label)).toEqual(['Coolors']);
|
|
});
|
|
|
|
it('returns an empty array when nothing matches', () => {
|
|
expect(filterReferenceGroups(REFERENCE_GROUPS, 'all', 'zzz-no-such-reference')).toEqual([]);
|
|
});
|
|
});
|