diff --git a/package.json b/package.json index 40faddfcd87..6de44f21f57 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build": "npm run generate:data && npm run validate:data && vite build && jiti scripts/prepare-pages-fallback.ts", "preview": "vite preview", "verify": "npx tsc --noEmit && node scripts/check_shortcode_syntax.js content && npm run build && npm run verify:artifact", - "test": "npm run test:visual-review && npm run test:ability-damage-evidence && tsx scripts/dev-review.test.ts && tsx scripts/asset-dump-resolver.test.ts && tsx scripts/blood-hunts.test.ts && tsx scripts/npc-portraits.test.ts && tsx scripts/buildable-portraits.test.ts && tsx --test src/pages/HomePage.test.tsx && tsx --test src/components/layout/SiteShell.test.tsx && tsx --test src/components/db/DbDetailView.test.tsx", + "test": "npm run test:visual-review && npm run test:ability-damage-evidence && tsx scripts/dev-review.test.ts && tsx scripts/asset-dump-resolver.test.ts && tsx scripts/blood-hunts.test.ts && tsx scripts/npc-portraits.test.ts && tsx scripts/buildable-portraits.test.ts && tsx --test src/pages/HomePage.test.tsx && tsx --test src/components/layout/SiteShell.test.tsx && tsx --test src/components/db/DbDetailView.test.tsx && tsx --test src/components/reference/ReferenceDetailView.test.tsx", "visual:baseline": "npm run build && npm run visual:baseline:capture", "visual:baseline:capture": "jiti scripts/visual-review.ts baseline", "visual:compare": "npm run build && npm run visual:compare:capture", diff --git a/scripts/build-reference-index.ts b/scripts/build-reference-index.ts index 1ab99be2038..1987e2b958a 100644 --- a/scripts/build-reference-index.ts +++ b/scripts/build-reference-index.ts @@ -779,6 +779,7 @@ async function main() { } function registerPrefabReference(doc: SourceDocument, guid: number | null, categories: string[], components: ParsedPrefabComponent[]): void { + const componentDocCount = components.filter((component) => component.path).length; const componentRelations = components.map((component) => ({ title: component.name, path: component.path, @@ -837,6 +838,7 @@ async function main() { toRow("GUID", guid, { monospace: true }), toRow("Categories", categories.length), toRow("Components", components.length), + toRow("Component Docs", `${componentDocCount} / ${components.length}`), toRow("Nested Entry Blocks", components.filter((component) => component.entries.length > 0).length) ].filter((row): row is ReferenceFieldRow => Boolean(row)), detailSections, diff --git a/src/components/reference/ReferenceDetailView.test.tsx b/src/components/reference/ReferenceDetailView.test.tsx new file mode 100644 index 00000000000..c3d75408835 --- /dev/null +++ b/src/components/reference/ReferenceDetailView.test.tsx @@ -0,0 +1,78 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { MemoryRouter } from "react-router-dom"; +import { ReferenceDetail } from "../../types/reference"; +import { ReferenceDetailView } from "./ReferenceDetailView"; + +function renderWithoutLayoutWarning(render: () => string) { + const originalError = console.error; + console.error = (...args: unknown[]) => { + const first = String(args[0] ?? ""); + if (first.includes("useLayoutEffect does nothing on the server")) { + return; + } + + originalError(...args); + }; + + try { + return render(); + } finally { + console.error = originalError; + } +} + +function renderReferenceDetail(detail: ReferenceDetail) { + return renderWithoutLayoutWarning(() => + renderToStaticMarkup( + + + + ) + ); +} + +test("prefab component relations explain snapshot source and doc coverage", () => { + const detail: ReferenceDetail = { + section: "prefabs", + kind: "prefab", + slug: "char-test-vblood", + title: "CHAR_Test_VBlood", + path: "/prefabs/char-test-vblood", + sourcePath: "content/prefabs/CHAR_Test_VBlood.md", + excerpt: "Test prefab.", + tags: ["CHAR_Test_VBlood"], + stats: [ + { label: "Components", value: "2" }, + { label: "Component Docs", value: "1 / 2" } + ], + relationGroups: [ + { + title: "Components", + totalCount: 2, + items: [ + { title: "ProjectM.AbilityBar_Server", path: "/components/abilitybar-server", description: "10 fields" }, + { title: "ProjectM.IdleInteractor", description: "3 fields" } + ] + }, + { + title: "Collections", + totalCount: 1, + items: [{ title: "CHAR", path: "/prefabs/char", description: "Collection" }] + } + ] + }; + + const html = renderReferenceDetail(detail); + + assert.match(html, /2 components/); + assert.doesNotMatch(html, /2 linked/); + assert.match(html, /extracted prefab component snapshot/); + assert.match(html, /Linked rows open generated component docs/); + assert.match(html, /No component doc/); + assert.match(html, /href="\/components\/abilitybar-server"/); + assert.match(html, /Component Docs/); + assert.match(html, /1 \/ 2/); +}); diff --git a/src/components/reference/ReferenceDetailView.tsx b/src/components/reference/ReferenceDetailView.tsx index f4528dd167e..d1eafcb13db 100644 --- a/src/components/reference/ReferenceDetailView.tsx +++ b/src/components/reference/ReferenceDetailView.tsx @@ -2,7 +2,7 @@ import { DetailJumpItem, DetailJumpStrip } from "../common/DetailJumpStrip"; import { CollapsibleTextBlock } from "../common/CollapsibleTextBlock"; import { CopyValueButton } from "../common/CopyValueButton"; import { headingId } from "../../lib/text"; -import { ReferenceDetail, ReferenceRelationGroup } from "../../types/reference"; +import { ReferenceDetail, ReferenceRelation, ReferenceRelationGroup } from "../../types/reference"; import { ReferenceBadge, ReferenceFieldGrid, ReferenceRelationList, ReferenceSurface } from "./ReferenceUi"; function getMonogram(value: string): string { @@ -96,6 +96,42 @@ function buildJumpItems(detail: ReferenceDetail, relationGroups: ReferenceRelati return items; } +function isPrefabComponentsGroup(detail: ReferenceDetail, group: ReferenceRelationGroup): boolean { + return detail.section === "prefabs" && group.title === "Components"; +} + +function getRelationMeta(detail: ReferenceDetail, group: ReferenceRelationGroup): string { + const count = group.totalCount ?? group.items.length; + return isPrefabComponentsGroup(detail, group) ? `${count} components` : `${count} linked`; +} + +function getRelationItems(detail: ReferenceDetail, group: ReferenceRelationGroup): ReferenceRelation[] { + if (!isPrefabComponentsGroup(detail, group)) { + return group.items; + } + + return group.items.map((item) => + item.path + ? item + : { + ...item, + badges: item.badges?.includes("No component doc") ? item.badges : [...(item.badges ?? []), "No component doc"] + } + ); +} + +function renderPrefabComponentNote(detail: ReferenceDetail, group: ReferenceRelationGroup) { + if (!isPrefabComponentsGroup(detail, group)) { + return null; + } + + return ( +

+ Component rows come from an extracted prefab component snapshot. Linked rows open generated component docs; unlinked rows are still attached components without a matching component doc page. +

+ ); +} + function renderSummaryRows(detail: ReferenceDetail) { const stats = detail.stats ?? []; if (stats.length === 0) { @@ -185,9 +221,12 @@ export function ReferenceDetailView({ detail }: { detail: ReferenceDetail }) { key={group.title} title={group.title} anchorId={`relation-${headingId(group.title)}`} - meta={`${group.totalCount ?? group.items.length} linked`} + meta={getRelationMeta(detail, group)} > - +
+ {renderPrefabComponentNote(detail, group)} + +
))}