133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
deriveRecommendedTemplateSlugs,
|
|
gridEntriesWithFacetScores,
|
|
} from "../../lib/templates/templateGridPresentation";
|
|
import { fetchRankedTemplatesByFacets } from "../../lib/create/fetchTemplates";
|
|
import type { RuleTemplateDto } from "../../lib/create/fetchTemplates";
|
|
|
|
const minimalTemplate = (slug: string, title: string): RuleTemplateDto => ({
|
|
id: "x",
|
|
slug,
|
|
title,
|
|
category: null,
|
|
description: null,
|
|
body: null,
|
|
sortOrder: 0,
|
|
featured: false,
|
|
});
|
|
|
|
describe("deriveRecommendedTemplateSlugs", () => {
|
|
it("returns at most limit slugs in the top score tier (API order for ties)", () => {
|
|
const templates = ["a", "b", "c", "d", "e", "f"].map((s) => ({ slug: s }));
|
|
const scores = Object.fromEntries(
|
|
["a", "b", "c", "d", "e", "f"].map((s) => [
|
|
s,
|
|
{ score: 1, matchedFacets: [] as string[] },
|
|
]),
|
|
);
|
|
const set = deriveRecommendedTemplateSlugs(templates, scores, 5);
|
|
expect(set.size).toBe(5);
|
|
expect([...set]).toEqual(["a", "b", "c", "d", "e"]);
|
|
});
|
|
|
|
it("only recommends the highest score group, not lower tiers to fill the cap", () => {
|
|
const templates = ["a", "b", "c", "d", "e"].map((s) => ({ slug: s }));
|
|
const scores = {
|
|
a: { score: 4, matchedFacets: [] as string[] },
|
|
b: { score: 4, matchedFacets: [] as string[] },
|
|
c: { score: 3, matchedFacets: [] as string[] },
|
|
d: { score: 3, matchedFacets: [] as string[] },
|
|
e: { score: 3, matchedFacets: [] as string[] },
|
|
};
|
|
const set = deriveRecommendedTemplateSlugs(templates, scores, 5);
|
|
expect([...set]).toEqual(["a", "b"]);
|
|
});
|
|
});
|
|
|
|
describe("gridEntriesWithFacetScores", () => {
|
|
it("sets recommended true only for top compact matches (like card decks)", () => {
|
|
const t = minimalTemplate("do-ocracy", "Do-ocracy");
|
|
const [row] = gridEntriesWithFacetScores([t], {
|
|
"do-ocracy": { score: 3, matchedFacets: ["a"] },
|
|
});
|
|
expect(row.recommended).toBe(true);
|
|
});
|
|
|
|
it("does not mark lower-scoring templates recommended when a higher tier exists", () => {
|
|
const high = [
|
|
minimalTemplate("a", "A"),
|
|
minimalTemplate("b", "B"),
|
|
];
|
|
const low = minimalTemplate("c", "C");
|
|
const rows = gridEntriesWithFacetScores([...high, low], {
|
|
a: { score: 4, matchedFacets: [] },
|
|
b: { score: 4, matchedFacets: [] },
|
|
c: { score: 3, matchedFacets: [] },
|
|
});
|
|
const rec = rows.filter((r) => r.recommended).map((r) => r.slug);
|
|
expect(rec).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("caps top-tier recommended badges to compactRecommendedLimit", () => {
|
|
const slugs = ["a", "b", "c", "d", "e", "f"];
|
|
const templates = slugs.map((s) => minimalTemplate(s, s));
|
|
const scores = Object.fromEntries(
|
|
slugs.map((s) => [s, { score: 1, matchedFacets: [] as string[] }]),
|
|
);
|
|
const rows = gridEntriesWithFacetScores(templates, scores, {
|
|
compactRecommendedLimit: 5,
|
|
});
|
|
expect(rows.filter((r) => r.recommended).map((r) => r.slug)).toEqual([
|
|
"a",
|
|
"b",
|
|
"c",
|
|
"d",
|
|
"e",
|
|
]);
|
|
});
|
|
|
|
it("sets recommended false when score is zero or missing", () => {
|
|
const t = minimalTemplate("consensus", "Consensus");
|
|
const [a] = gridEntriesWithFacetScores([t], {
|
|
consensus: { score: 0, matchedFacets: [] },
|
|
});
|
|
const [b] = gridEntriesWithFacetScores([t], {});
|
|
expect(a.recommended).toBe(false);
|
|
expect(b.recommended).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("fetchRankedTemplatesByFacets", () => {
|
|
beforeEach(() => {
|
|
global.fetch = vi.fn();
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("parses ok JSON with templates and scores", async () => {
|
|
const template = minimalTemplate("s", "T");
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
templates: [template],
|
|
scores: { s: { score: 1, matchedFacets: ["size:oneMember"] } },
|
|
}),
|
|
} as Response);
|
|
const r = await fetchRankedTemplatesByFacets({
|
|
facetQuery: "facet.size=oneMember",
|
|
});
|
|
expect("error" in r).toBe(false);
|
|
if (!("error" in r)) {
|
|
expect(r.templates).toEqual([template]);
|
|
expect(r.scores.s?.score).toBe(1);
|
|
}
|
|
});
|
|
|
|
it("returns error when facetQuery is empty", async () => {
|
|
const r = await fetchRankedTemplatesByFacets({ facetQuery: "" });
|
|
expect("error" in r).toBe(true);
|
|
});
|
|
});
|