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
342 lines
15 KiB
TypeScript
342 lines
15 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']);
|
|
});
|
|
|
|
// Deck scenes are the 15 commercial "品类" buckets, resolved from the plugin's
|
|
// commercial category id (od.category / a category tag / od.scenario) rather
|
|
// than tag-slug heuristics, so the filter row shares one taxonomy with the
|
|
// per-card category chip.
|
|
it('maps deck templates to their commercial scene by resolved category id', () => {
|
|
expect(extractSubcategories(fixture({ id: 'pitch', od: { mode: 'deck', category: 'fundraising-pitch' } }))).toEqual(['fundraising-pitch']);
|
|
expect(extractSubcategories(fixture({ id: 'board', od: { mode: 'deck', category: 'corporate-strategy' } }))).toEqual(['corporate-strategy']);
|
|
expect(extractSubcategories(fixture({ id: 'sales', od: { mode: 'deck', category: 'b2b-sales' } }))).toEqual(['b2b-sales']);
|
|
expect(extractSubcategories(fixture({ id: 'craft', od: { mode: 'deck', category: 'design-craft' } }))).toEqual(['design-craft']);
|
|
// A category tag resolves the scene when od.category is absent.
|
|
expect(extractSubcategories(fixture({ id: 'tagged', tags: ['ai-literacy'], od: { mode: 'deck' } }))).toEqual(['ai-literacy']);
|
|
// A deck with no resolvable commercial category lands in no scene bucket.
|
|
expect(extractSubcategories(fixture({ id: 'bare', tags: ['pitch-deck'], od: { mode: 'deck' } }))).toEqual([]);
|
|
});
|
|
|
|
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']);
|
|
});
|
|
|
|
// Regression: the prototype/image/video rail display order
|
|
// (SUBCATEGORY_DISPLAY_ORDER) must NOT change which bucket an overlapping-tag
|
|
// plugin lands in. Bucketing is decided by SUBCATEGORIES matching precedence,
|
|
// which stays stable even though Brand / design renders first in the rails.
|
|
// (Decks are exempt: their bucket is the single resolved commercial category.)
|
|
it('keeps bucket membership stable for overlapping-tag plugins regardless of display order', () => {
|
|
// `dashboard` + `design`: stays in Dashboards (not Brand / design).
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'dash-glass', tags: ['dashboard', 'design'], od: { mode: 'prototype' } })),
|
|
).toEqual(['business-dashboards']);
|
|
// mobile app + `design`: stays in Apps (not Brand / design).
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'mobile', tags: ['mobile-app', 'design'], od: { mode: 'prototype' } })),
|
|
).toEqual(['app-prototypes']);
|
|
// landing + `brand`: stays in Landing / marketing (not Brand / design).
|
|
expect(
|
|
extractSubcategories(fixture({ id: 'landing-brand', tags: ['saas-landing', 'brand'], od: { mode: 'prototype' } })),
|
|
).toEqual(['landing-marketing']);
|
|
});
|
|
|
|
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([
|
|
['deck', 1],
|
|
['prototype', 1],
|
|
['live-artifact', 1],
|
|
['image', 1],
|
|
['video', 1],
|
|
['hyperframes', 1],
|
|
['audio', 1],
|
|
]);
|
|
// Display order (SUBCATEGORY_DISPLAY_ORDER) — distinct from the matching
|
|
// precedence encoded by the SUBCATEGORIES array order.
|
|
expect((catalog.subcategory.prototype ?? []).map((o) => o.slug)).toEqual([
|
|
'landing-marketing',
|
|
'brand-design',
|
|
'business-dashboards',
|
|
'app-prototypes',
|
|
'developer-tools',
|
|
'docs-reports',
|
|
]);
|
|
// Deck scenes: the 15 commercial "品类" buckets in commercial-priority order.
|
|
expect((catalog.subcategory.deck ?? []).map((o) => o.slug)).toEqual([
|
|
'fundraising-pitch',
|
|
'corporate-strategy',
|
|
'b2b-sales',
|
|
'product-management',
|
|
'design-craft',
|
|
'marketing-gtm',
|
|
'data-finance',
|
|
'consulting',
|
|
'government-policy',
|
|
'professional-training',
|
|
'academic-research',
|
|
'ai-literacy',
|
|
'career',
|
|
'student-coursework',
|
|
'life',
|
|
]);
|
|
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', od: { mode: 'deck', category: 'fundraising-pitch' } }),
|
|
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']);
|
|
// Deck scene bucket = the plugin's resolved commercial category.
|
|
expect(
|
|
applyFacetSelection(plugins, { category: 'deck', subcategory: 'fundraising-pitch' }).map((p) => p.id),
|
|
).toEqual(['deck']);
|
|
});
|
|
});
|
|
|
|
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 Slides when that bucket exists', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'slides', od: { mode: 'deck' } }),
|
|
fixture({ id: 'prototype', od: { mode: 'prototype' } }),
|
|
]);
|
|
|
|
expect(resolveDefaultSelection(catalog)).toEqual({
|
|
category: 'deck',
|
|
subcategory: null,
|
|
});
|
|
});
|
|
|
|
it('falls back to the first populated artifact kind when Slides is unavailable', () => {
|
|
const catalog = buildFacetCatalog([
|
|
fixture({ id: 'prototype', od: { mode: 'prototype' } }),
|
|
]);
|
|
|
|
expect(resolveDefaultSelection(catalog)).toEqual({
|
|
category: 'prototype',
|
|
subcategory: null,
|
|
});
|
|
});
|
|
});
|