open-design/apps/web/tests/components/plugins-home-visualScore.test.ts
Eli-tangerine 72c8e34bc9
Polish home onboarding and community presets (#2658)
Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-22 14:26:56 +08:00

189 lines
6.1 KiB
TypeScript

// 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<string, unknown>;
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']);
});
});