// Visual-appeal ranking contract for the plugins-home gallery. // // The home grid relies on `sortByVisualAppeal` (and the underlying // `pluginVisualScore`) to surface cinematic decks / image / video // templates above plain scenario plugins. These tests lock the // ordering so the first viewport keeps leading with rich previews // instead of regressing back to alphabetical bundled noise. import { describe, expect, it } from 'vitest'; import type { InstalledPluginRecord } from '@open-design/contracts'; import { pluginVisualScore, sortByVisualAppeal, } from '../../src/components/plugins-home/visualScore'; function fixture(overrides: { id: string; title?: string; description?: string; tags?: string[]; od?: Record; author?: string; }): 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.description ? { description: overrides.description } : {}), ...(overrides.tags ? { tags: overrides.tags } : {}), ...(overrides.author ? { author: { name: overrides.author } } : {}), ...(overrides.od ? { od: overrides.od } : {}), }, fsPath: '/tmp', installedAt: 0, updatedAt: 0, }; } describe('pluginVisualScore', () => { it('boosts featured plugins above everything else', () => { const plain = fixture({ id: 'plain' }); const featured = fixture({ id: 'featured', od: { featured: true } }); expect(pluginVisualScore(featured)).toBeGreaterThan( pluginVisualScore(plain) + 500, ); }); it('uses numeric featured ranks to order curated picks', () => { const lead = fixture({ id: 'lead', od: { featured: 2 } }); const later = fixture({ id: 'later', od: { featured: 19 } }); expect(pluginVisualScore(lead)).toBeGreaterThan(pluginVisualScore(later)); }); it('ranks media-rich plugins above plain scenarios', () => { const text = fixture({ id: 'text' }); const deckHtml = fixture({ id: 'deck', od: { mode: 'deck', preview: { type: 'html', entry: './index.html' } }, }); const image = fixture({ id: 'image', od: { surface: 'image', mode: 'image', preview: { type: 'image', poster: 'a.png' } }, }); const video = fixture({ id: 'video', od: { surface: 'video', mode: 'video', preview: { type: 'video', video: 'a.mp4' } }, }); expect(pluginVisualScore(video)).toBeGreaterThan(pluginVisualScore(image)); expect(pluginVisualScore(image)).toBeGreaterThan(pluginVisualScore(deckHtml)); expect(pluginVisualScore(deckHtml)).toBeGreaterThan(pluginVisualScore(text)); }); it('credits design-system plugins between decks and plain text', () => { const text = fixture({ id: 'text' }); const ds = fixture({ id: 'ds', od: { mode: 'design-system' } }); expect(pluginVisualScore(ds)).toBeGreaterThan(pluginVisualScore(text)); }); it('penalises atom kind so they never accidentally lead the grid', () => { const atom = fixture({ id: 'a', od: { kind: 'atom' } }); expect(pluginVisualScore(atom)).toBeLessThan(0); }); }); describe('sortByVisualAppeal', () => { it('places the cinematic deck first, plain scenarios last', () => { const records = [ fixture({ id: 'plain' }), fixture({ id: 'guizang-ppt', od: { mode: 'deck', preview: { type: 'html', entry: './index.html' } }, }), fixture({ id: 'photo', od: { surface: 'image', mode: 'image', preview: { type: 'image', poster: 'p.png' }, }, }), fixture({ id: 'reel', od: { surface: 'video', mode: 'video', preview: { type: 'video', video: 'r.mp4', poster: 'r.png' }, featured: true, }, }), ]; const sorted = sortByVisualAppeal(records).map((r) => r.id); expect(sorted[0]).toBe('reel'); expect(sorted[sorted.length - 1]).toBe('plain'); }); it('puts curated home picks before generic featured media', () => { const records = [ fixture({ id: 'featured-video', od: { surface: 'video', mode: 'video', preview: { type: 'video', video: 'r.mp4', poster: 'r.png' }, featured: true, }, }), fixture({ id: 'example-open-design-landing', od: { mode: 'prototype', preview: { type: 'html', entry: './index.html' } }, }), ]; const sorted = sortByVisualAppeal(records).map((r) => r.id); expect(sorted[0]).toBe('example-open-design-landing'); }); it('keeps numeric featured rank ahead of media bonuses', () => { const records = [ fixture({ id: 'hyperframes', od: { surface: 'video', mode: 'video', preview: { type: 'video', video: 'r.mp4', poster: 'r.png' }, featured: 0.13, }, }), fixture({ id: 'guizang', od: { mode: 'deck', preview: { type: 'html', entry: './index.html' }, featured: 0.01, }, }), fixture({ id: 'huashu', od: { mode: 'prototype', featured: 0.03 } }), fixture({ id: 'kami', od: { mode: 'deck', featured: 0.06 } }), ]; const sorted = sortByVisualAppeal(records).map((r) => r.id); expect(sorted).toEqual(['guizang', 'huashu', 'kami', 'hyperframes']); }); it('keeps the original list reference unchanged (returns a new array)', () => { const records = [ fixture({ id: 'a' }), fixture({ id: 'b', od: { mode: 'deck' } }), ]; const before = records.map((r) => r.id).join(','); sortByVisualAppeal(records); expect(records.map((r) => r.id).join(',')).toBe(before); }); it('breaks ties deterministically by title, then by original position', () => { const records = [ fixture({ id: 'beta', title: 'Beta' }), fixture({ id: 'alpha', title: 'Alpha' }), ]; const sorted = sortByVisualAppeal(records).map((r) => r.id); expect(sorted).toEqual(['alpha', 'beta']); }); });