mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
299 lines
13 KiB
TypeScript
299 lines
13 KiB
TypeScript
// Facet derivation contract for the plugins-home filter row. The
|
|
// home section is driven by artifact-kind primary tabs that mirror the
|
|
// artifact creation surface, plus scene buckets derived from the
|
|
// user-query taxonomy for the crowded template types.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { InstalledPluginRecord } from '@open-design/contracts';
|
|
import {
|
|
applyFacetSelection,
|
|
buildFacetCatalog,
|
|
extractCategories,
|
|
extractSubcategories,
|
|
isFeaturedPlugin,
|
|
resolveDefaultSelection,
|
|
} from '../../src/components/plugins-home/facets';
|
|
|
|
function fixture(overrides: {
|
|
id: string;
|
|
title?: string;
|
|
tags?: string[];
|
|
od?: Record<string, unknown>;
|
|
}): InstalledPluginRecord {
|
|
return {
|
|
id: overrides.id,
|
|
title: overrides.title ?? overrides.id,
|
|
version: '0.1.0',
|
|
sourceKind: 'bundled',
|
|
source: '/tmp',
|
|
trust: 'bundled',
|
|
capabilitiesGranted: ['prompt:inject'],
|
|
manifest: {
|
|
name: overrides.id,
|
|
version: '0.1.0',
|
|
...(overrides.tags ? { tags: overrides.tags } : {}),
|
|
...(overrides.od ? { od: overrides.od } : {}),
|
|
},
|
|
fsPath: '/tmp',
|
|
installedAt: 0,
|
|
updatedAt: 0,
|
|
};
|
|
}
|
|
|
|
describe('extractCategories', () => {
|
|
it('maps generation modes to artifact-kind primary tabs', () => {
|
|
expect(extractCategories(fixture({ id: 'prototype', od: { mode: 'prototype' } }))).toEqual(['prototype']);
|
|
expect(extractCategories(fixture({ id: 'deck', od: { mode: 'deck' } }))).toEqual(['deck']);
|
|
expect(extractCategories(fixture({ id: 'image', od: { mode: 'image' } }))).toEqual(['image']);
|
|
expect(extractCategories(fixture({ id: 'video', od: { mode: 'video' } }))).toEqual(['video']);
|
|
expect(extractCategories(fixture({ id: 'audio', od: { mode: 'audio' } }))).toEqual(['audio']);
|
|
});
|
|
|
|
it('groups live artifacts ahead of their underlying rendering mode', () => {
|
|
expect(
|
|
extractCategories(
|
|
fixture({
|
|
id: 'example-live-dashboard',
|
|
tags: ['live-dashboard'],
|
|
od: { mode: 'prototype' },
|
|
}),
|
|
),
|
|
).toEqual(['live-artifact']);
|
|
expect(
|
|
extractCategories(
|
|
fixture({
|
|
id: 'image-template-notion-team-dashboard-live-artifact',
|
|
tags: ['live-artifact'],
|
|
od: { mode: 'image' },
|
|
}),
|
|
),
|
|
).toEqual(['live-artifact']);
|
|
expect(
|
|
extractCategories(
|
|
fixture({
|
|
id: 'example-social-media-matrix-tracker-template',
|
|
tags: ['live-artifacts'],
|
|
od: { mode: 'template' },
|
|
}),
|
|
),
|
|
).toEqual(['live-artifact']);
|
|
});
|
|
|
|
it('splits HyperFrames from the broader video mode', () => {
|
|
expect(
|
|
extractCategories(fixture({ id: 'hf', tags: ['hyperframes'], od: { mode: 'video' } })),
|
|
).toEqual(['hyperframes']);
|
|
expect(
|
|
extractCategories(fixture({ id: 'composition', tags: ['video-composition'], od: { mode: 'video' } })),
|
|
).toEqual(['hyperframes']);
|
|
});
|
|
|
|
it('keeps non-artifact workflow and design-system plugins out of primary tabs', () => {
|
|
expect(extractCategories(fixture({ id: 'design-system', od: { mode: 'design-system' } }))).toEqual([]);
|
|
expect(extractCategories(fixture({ id: 'import', od: { taskKind: 'figma-migration', mode: 'scenario' } }))).toEqual([]);
|
|
expect(extractCategories(fixture({ id: 'export', tags: ['export', 'react'], od: { mode: 'export' } }))).toEqual([]);
|
|
expect(extractCategories(fixture({ id: 'utility', od: { mode: 'utility' } }))).toEqual([]);
|
|
});
|
|
|
|
it('normalises mode casing / formatting via slugify before matching', () => {
|
|
expect(extractCategories(fixture({ id: 'a', od: { mode: 'Prototype' } }))).toEqual(['prototype']);
|
|
expect(extractCategories(fixture({ id: 'b', od: { mode: 'slide_deck' } }))).toEqual([]);
|
|
expect(extractCategories(fixture({ id: 'c', od: { mode: 'deck' } }))).toEqual(['deck']);
|
|
});
|
|
});
|
|
|
|
describe('extractSubcategories', () => {
|
|
it('maps prototype templates to prompt-taxonomy scene buckets', () => {
|
|
expect(extractSubcategories(fixture({ id: 'dashboard', tags: ['dashboard'], od: { mode: 'prototype' } }))).toEqual(['business-dashboards']);
|
|
expect(extractSubcategories(fixture({ id: 'app', tags: ['mobile-app'], od: { mode: 'prototype' } }))).toEqual(['app-prototypes']);
|
|
expect(extractSubcategories(fixture({ id: 'landing', tags: ['saas-landing'], od: { mode: 'prototype' } }))).toEqual(['landing-marketing']);
|
|
expect(extractSubcategories(fixture({ id: 'dev', tags: ['engineering'], od: { mode: 'prototype' } }))).toEqual(['developer-tools']);
|
|
expect(extractSubcategories(fixture({ id: 'clinical', tags: ['case-report'], od: { mode: 'prototype' } }))).toEqual(['docs-reports']);
|
|
expect(extractSubcategories(fixture({ id: 'brand', tags: ['wireframe'], od: { mode: 'prototype' } }))).toEqual(['brand-design']);
|
|
});
|
|
|
|
it('maps deck templates to pitch, course, report, product, engineering, and creative scenes', () => {
|
|
expect(extractSubcategories(fixture({ id: 'pitch', tags: ['pitch-deck'], od: { mode: 'deck' } }))).toEqual(['pitch-business']);
|
|
expect(extractSubcategories(fixture({ id: 'course', tags: ['course-module'], od: { mode: 'deck' } }))).toEqual(['course-training']);
|
|
expect(extractSubcategories(fixture({ id: 'report', tags: ['weekly-report'], od: { mode: 'deck' } }))).toEqual(['reports-briefings']);
|
|
expect(extractSubcategories(fixture({ id: 'launch', tags: ['product-launch'], od: { mode: 'deck' } }))).toEqual(['product-sales']);
|
|
expect(extractSubcategories(fixture({ id: 'tech', tags: ['tech-sharing'], od: { mode: 'deck' } }))).toEqual(['engineering-talks']);
|
|
expect(extractSubcategories(fixture({ id: 'creative', tags: ['zhangzara'], od: { mode: 'deck' } }))).toEqual(['creative-decks']);
|
|
});
|
|
|
|
it('maps image templates to visual-scene buckets', () => {
|
|
expect(extractSubcategories(fixture({ id: 'ui', tags: ['app-web-design'], od: { mode: 'image' } }))).toEqual(['ui-product-mockups']);
|
|
expect(extractSubcategories(fixture({ id: 'brand', tags: ['typography'], od: { mode: 'image' } }))).toEqual(['brand-visuals']);
|
|
expect(extractSubcategories(fixture({ id: 'storyboard', tags: ['storyboard'], od: { mode: 'image' } }))).toEqual(['storyboards-motion-refs']);
|
|
expect(extractSubcategories(fixture({ id: 'social', tags: ['social-media-post'], od: { mode: 'image' } }))).toEqual(['social-content']);
|
|
expect(extractSubcategories(fixture({ id: 'portrait', tags: ['profile-avatar'], od: { mode: 'image' } }))).toEqual(['avatar-portrait']);
|
|
expect(extractSubcategories(fixture({ id: 'illustration', tags: ['illustration'], od: { mode: 'image' } }))).toEqual(['illustration-style']);
|
|
});
|
|
|
|
it('maps non-HyperFrames video templates to scene buckets', () => {
|
|
expect(extractSubcategories(fixture({ id: 'motion', tags: ['motion-graphics'], od: { mode: 'video' } }))).toEqual(['motion-effects']);
|
|
expect(extractSubcategories(fixture({ id: 'social', tags: ['short-form'], od: { mode: 'video' } }))).toEqual(['social-short-form']);
|
|
expect(extractSubcategories(fixture({ id: 'marketing', tags: ['product-promo'], od: { mode: 'video' } }))).toEqual(['marketing-product']);
|
|
expect(extractSubcategories(fixture({ id: 'data', tags: ['flowchart'], od: { mode: 'video' } }))).toEqual(['data-explainers']);
|
|
expect(extractSubcategories(fixture({ id: 'cinema', tags: ['cinematic'], od: { mode: 'video' } }))).toEqual(['cinematic-story']);
|
|
});
|
|
|
|
it('keeps Live Artifact, HyperFrames, and Audio flat with no second-level buckets', () => {
|
|
expect(
|
|
extractSubcategories(
|
|
fixture({
|
|
id: 'example-live-artifact',
|
|
tags: ['live-artifact'],
|
|
od: { mode: 'prototype' },
|
|
}),
|
|
),
|
|
).toEqual([]);
|
|
expect(extractSubcategories(fixture({ id: 'hf', tags: ['hyperframes'], od: { mode: 'video' } }))).toEqual([]);
|
|
expect(extractSubcategories(fixture({ id: 'audio', od: { mode: 'audio' } }))).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('buildFacetCatalog', () => {
|
|
it('produces artifact-kind primary tabs in product order', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'prototype', tags: ['dashboard'], od: { mode: 'prototype' } }),
|
|
fixture({ id: 'example-live-artifact', tags: ['live-artifact'], od: { mode: 'prototype' } }),
|
|
fixture({ id: 'deck', tags: ['pitch-deck'], od: { mode: 'deck' } }),
|
|
fixture({ id: 'image', tags: ['profile-avatar'], od: { mode: 'image' } }),
|
|
fixture({ id: 'video', tags: ['cinematic'], od: { mode: 'video' } }),
|
|
fixture({ id: 'hf', tags: ['hyperframes'], od: { mode: 'video' } }),
|
|
fixture({ id: 'audio', od: { mode: 'audio' } }),
|
|
fixture({ id: 'design-system', od: { mode: 'design-system' } }),
|
|
]);
|
|
|
|
expect(catalog.category.map((o) => [o.slug, o.count])).toEqual([
|
|
['prototype', 1],
|
|
['live-artifact', 1],
|
|
['deck', 1],
|
|
['image', 1],
|
|
['video', 1],
|
|
['hyperframes', 1],
|
|
['audio', 1],
|
|
]);
|
|
expect((catalog.subcategory.prototype ?? []).map((o) => o.slug)).toEqual([
|
|
'business-dashboards',
|
|
'app-prototypes',
|
|
'landing-marketing',
|
|
'developer-tools',
|
|
'docs-reports',
|
|
'brand-design',
|
|
]);
|
|
expect((catalog.subcategory.deck ?? []).map((o) => o.slug)).toEqual([
|
|
'pitch-business',
|
|
'course-training',
|
|
'reports-briefings',
|
|
'product-sales',
|
|
'engineering-talks',
|
|
'creative-decks',
|
|
]);
|
|
expect((catalog.subcategory.image ?? []).map((o) => o.slug)).toEqual([
|
|
'ui-product-mockups',
|
|
'brand-visuals',
|
|
'storyboards-motion-refs',
|
|
'social-content',
|
|
'avatar-portrait',
|
|
'illustration-style',
|
|
]);
|
|
expect((catalog.subcategory.video ?? []).map((o) => o.slug)).toEqual([
|
|
'motion-effects',
|
|
'social-short-form',
|
|
'marketing-product',
|
|
'data-explainers',
|
|
'cinematic-story',
|
|
]);
|
|
expect(catalog.subcategory['live-artifact']).toBeUndefined();
|
|
expect(catalog.subcategory.hyperframes).toBeUndefined();
|
|
expect(catalog.subcategory.audio).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('applyFacetSelection', () => {
|
|
const plugins = [
|
|
fixture({ id: 'prototype-dashboard', tags: ['dashboard'], od: { mode: 'prototype' } }),
|
|
fixture({ id: 'prototype-app', tags: ['mobile-app'], od: { mode: 'prototype' } }),
|
|
fixture({ id: 'example-live-artifact', tags: ['live-artifact'], od: { mode: 'prototype' } }),
|
|
fixture({ id: 'deck', tags: ['pitch-deck'], od: { mode: 'deck' } }),
|
|
fixture({ id: 'image', tags: ['profile-avatar'], od: { mode: 'image' } }),
|
|
fixture({ id: 'video', tags: ['cinematic'], od: { mode: 'video' } }),
|
|
fixture({ id: 'hf', tags: ['hyperframes'], od: { mode: 'video' } }),
|
|
fixture({ id: 'audio', od: { mode: 'audio' } }),
|
|
];
|
|
|
|
it('returns everything when no category is selected', () => {
|
|
expect(
|
|
applyFacetSelection(plugins, { category: null, subcategory: null }).map((p) => p.id),
|
|
).toEqual([
|
|
'prototype-dashboard',
|
|
'prototype-app',
|
|
'example-live-artifact',
|
|
'deck',
|
|
'image',
|
|
'video',
|
|
'hf',
|
|
'audio',
|
|
]);
|
|
});
|
|
|
|
it('filters by the selected artifact-kind category slug', () => {
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'prototype', subcategory: null }).map((p) => p.id),
|
|
).toEqual(['prototype-dashboard', 'prototype-app']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'live-artifact', subcategory: null }).map((p) => p.id),
|
|
).toEqual(['example-live-artifact']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'hyperframes', subcategory: null }).map((p) => p.id),
|
|
).toEqual(['hf']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'video', subcategory: null }).map((p) => p.id),
|
|
).toEqual(['video']);
|
|
});
|
|
|
|
it('filters by the selected scene bucket inside the selected artifact kind', () => {
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'prototype', subcategory: 'business-dashboards' }).map((p) => p.id),
|
|
).toEqual(['prototype-dashboard']);
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'prototype', subcategory: 'app-prototypes' }).map((p) => p.id),
|
|
).toEqual(['prototype-app']);
|
|
});
|
|
});
|
|
|
|
describe('isFeaturedPlugin', () => {
|
|
it('returns true for boolean featured picks and numeric curator ranks', () => {
|
|
expect(isFeaturedPlugin(fixture({ id: 'a', od: { featured: true } }))).toBe(true);
|
|
expect(isFeaturedPlugin(fixture({ id: 'ranked', od: { featured: 4 } }))).toBe(true);
|
|
expect(isFeaturedPlugin(fixture({ id: 'b', od: { featured: 'true' } }))).toBe(false);
|
|
expect(isFeaturedPlugin(fixture({ id: 'c' }))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('resolveDefaultSelection', () => {
|
|
it('defaults the home catalog to Prototype when that bucket exists', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'slides', od: { mode: 'deck' } }),
|
|
fixture({ id: 'prototype', od: { mode: 'prototype' } }),
|
|
]);
|
|
|
|
expect(resolveDefaultSelection(catalog)).toEqual({
|
|
category: 'prototype',
|
|
subcategory: null,
|
|
});
|
|
});
|
|
|
|
it('falls back to the first populated artifact kind when Prototype is unavailable', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'slides', od: { mode: 'deck' } }),
|
|
]);
|
|
|
|
expect(resolveDefaultSelection(catalog)).toEqual({
|
|
category: 'deck',
|
|
subcategory: null,
|
|
});
|
|
});
|
|
});
|