first
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { RightDrawer } from "../src/components/RightDrawer";
|
||||
import {
|
||||
graphCommunitySummaryDrawer,
|
||||
graphEmptyDrawer,
|
||||
graphErrorDrawer,
|
||||
graphExcludedObjectDrawer,
|
||||
graphLoadingDrawer,
|
||||
graphNodeSummaryDrawer,
|
||||
graphReaderDrawer,
|
||||
graphUnavailableObjectDrawer,
|
||||
type DrawerState,
|
||||
} from "../src/lib/drawer-state";
|
||||
import type {
|
||||
GraphCommunitySummaryPayload,
|
||||
GraphExcludedObjectPayload,
|
||||
GraphNodeSummaryPayload,
|
||||
GraphUnavailableObjectPayload,
|
||||
} from "@llm-wiki/graph-engine";
|
||||
|
||||
describe("RightDrawer graph lightweight summaries", () => {
|
||||
it("renders node summary fields and actions without full reader markdown", () => {
|
||||
const html = renderDrawer(graphNodeSummaryDrawer(nodeSummaryFixture()));
|
||||
|
||||
assert.match(html, /data-testid="graph-node-summary"/);
|
||||
assert.match(html, /Alpha node/);
|
||||
assert.match(html, /节点/);
|
||||
assert.match(html, /连接/);
|
||||
assert.match(html, /graph-summary-community-chip/);
|
||||
assert.match(html, /graph-summary-relation-pill/);
|
||||
assert.match(html, /打开详情/);
|
||||
assert.match(html, /固定位置/);
|
||||
assert.doesNotMatch(html, /full markdown body should stay out of summaries/);
|
||||
assert.doesNotMatch(html, /graph-reader-drawer/);
|
||||
assert.doesNotMatch(html, /summary-left-rail|ai-slop|border-left/);
|
||||
});
|
||||
|
||||
it("renders unified community drawer with overview, fixed actions, core nodes, and dialogue controls", () => {
|
||||
const html = renderDrawer(graphCommunitySummaryDrawer(communitySummaryFixture()));
|
||||
|
||||
assert.match(html, /data-testid="graph-community-summary"/);
|
||||
assert.match(html, /Alpha community/);
|
||||
assert.match(html, /graph-group-meta-row/);
|
||||
assert.match(html, /graph-group-meta-row[\s\S]*graph-summary-kicker[^>]*>社区<\/span>/);
|
||||
assert.match(html, /graph-group-meta-row[\s\S]*graph-group-status-chip[^>]*>结构清晰<\/span>/);
|
||||
assert.match(html, /graph-group-hero[\s\S]*graph-group-enter[^>]*>进入社区<\/button>/);
|
||||
assert.match(html, /进入社区/);
|
||||
assert.match(html, /总结这一簇/);
|
||||
assert.match(html, /找知识缺口/);
|
||||
assert.match(html, /生成主题页/);
|
||||
assert.match(html, /探索潜在关系/);
|
||||
assert.match(html, /补充说明(可选)/);
|
||||
assert.match(html, /发送/);
|
||||
assert.match(html, /新对话/);
|
||||
assert.match(html, /Alpha node/);
|
||||
assert.match(html, /当前社区会带入对话/);
|
||||
assert.match(html, /graph-group-node-toggle/);
|
||||
assert.match(html, /data-group-drawer="send"[\s\S]*<svg/);
|
||||
assert.match(html, /data-group-drawer="new-conversation"[\s\S]*<svg/);
|
||||
assert.doesNotMatch(html, /暂无搜索命中/);
|
||||
assert.doesNotMatch(html, /暂无固定节点/);
|
||||
assert.doesNotMatch(html, /暂无桥接关系/);
|
||||
});
|
||||
|
||||
it("renders ungrouped community without enter-community and recommends relation exploration", () => {
|
||||
const html = renderDrawer(graphCommunitySummaryDrawer(communitySummaryFixture({
|
||||
communityId: "_none",
|
||||
label: "未分组",
|
||||
structureState: "ungrouped",
|
||||
description: "这些页面暂未形成明确社区。你可以让 agent 探索它们之间是否存在潜在关系。",
|
||||
canEnterCommunity: false,
|
||||
commands: [],
|
||||
})));
|
||||
|
||||
assert.match(html, /未分组/);
|
||||
assert.match(html, /暂未形成明确社区/);
|
||||
assert.match(html, /当前社区会带入对话/);
|
||||
assert.match(html, /data-recommended="true"[^>]*>[\s\S]*探索潜在关系/);
|
||||
assert.doesNotMatch(html, /进入社区/);
|
||||
});
|
||||
|
||||
it("disables send until free text exists and enables it once typed", () => {
|
||||
const empty = renderDrawer(graphCommunitySummaryDrawer(communitySummaryFixture()));
|
||||
assert.match(empty, /<button[^>]*data-group-drawer="send"[^>]*disabled/);
|
||||
const filled = renderDrawer(graphCommunitySummaryDrawer(communitySummaryFixture(), "看一下缺口"));
|
||||
assert.doesNotMatch(filled, /<button[^>]*data-group-drawer="send"[^>]*disabled/);
|
||||
});
|
||||
|
||||
it("renders graph empty, excluded, and unavailable states", () => {
|
||||
const empty = renderDrawer(graphEmptyDrawer("没有搜索结果", "no-search-results", "暂无搜索结果"));
|
||||
const missingNeighbors = renderDrawer(graphEmptyDrawer("没有邻居", "missing-neighbors", "暂无邻居"));
|
||||
const missingCommunity = renderDrawer(graphEmptyDrawer("没有社区摘要", "missing-community-summary", "暂无社区摘要"));
|
||||
const missingStrongRelations = renderDrawer(graphEmptyDrawer("没有强关系", "missing-strong-relations", "暂无强关系"));
|
||||
const excluded = renderDrawer(graphExcludedObjectDrawer(excludedFixture()));
|
||||
const unavailable = renderDrawer(graphUnavailableObjectDrawer(unavailableFixture()));
|
||||
|
||||
assert.match(empty, /没有搜索结果/);
|
||||
assert.match(empty, /暂无搜索结果/);
|
||||
assert.match(missingNeighbors, /暂无邻居/);
|
||||
assert.match(missingCommunity, /暂无社区摘要/);
|
||||
assert.match(missingStrongRelations, /暂无强关系/);
|
||||
assert.match(excluded, /data-testid="graph-excluded-object"/);
|
||||
assert.match(excluded, /暂不可见/);
|
||||
assert.match(excluded, /当前筛选暂时隐藏了这个对象/);
|
||||
assert.match(excluded, /显示这个对象/);
|
||||
assert.match(excluded, /清除临时显示/);
|
||||
assert.match(unavailable, /data-testid="graph-unavailable-object"/);
|
||||
assert.match(unavailable, /missing-node/);
|
||||
assert.match(unavailable, /这个节点当前不可用/);
|
||||
});
|
||||
|
||||
it("renders loading, reader loading, reader error, and hard error states honestly", () => {
|
||||
const loading = renderDrawer(graphLoadingDrawer("整理图谱摘要", "正在整理当前对象"));
|
||||
const readerLoading = renderDrawer(graphReaderDrawer(graphPayload(), { loading: true }));
|
||||
const readerError = renderDrawer(graphReaderDrawer(graphPayload(), { error: "读取失败" }));
|
||||
const hardError = renderDrawer(graphErrorDrawer("图谱错误", "图谱服务暂时不可用"));
|
||||
|
||||
assert.match(loading, /data-testid="graph-simple-state"/);
|
||||
assert.match(loading, /整理图谱摘要/);
|
||||
assert.match(loading, /正在整理当前对象/);
|
||||
assert.match(readerLoading, /graph-reader-drawer/);
|
||||
assert.match(readerLoading, /加载中/);
|
||||
assert.doesNotMatch(readerLoading, /完整正文内容/);
|
||||
assert.match(readerError, /读取失败/);
|
||||
assert.doesNotMatch(readerError, /完整正文内容/);
|
||||
assert.match(hardError, /图谱错误/);
|
||||
assert.match(hardError, /图谱服务暂时不可用/);
|
||||
assert.doesNotMatch(`${loading}${readerLoading}${readerError}${hardError}`, /推荐阅读|猜你喜欢|建议你打开/);
|
||||
});
|
||||
|
||||
it("keeps drawer actions as tabbable buttons", () => {
|
||||
const node = renderDrawer(graphNodeSummaryDrawer(nodeSummaryFixture()));
|
||||
const community = renderDrawer(graphCommunitySummaryDrawer(communitySummaryFixture()));
|
||||
const excluded = renderDrawer(graphExcludedObjectDrawer(excludedFixture()));
|
||||
|
||||
assert.match(node, /<button[^>]*class="graph-summary-action"[^>]*>打开详情<\/button>/);
|
||||
assert.match(node, /<button[^>]*class="graph-summary-action"[^>]*>固定位置<\/button>/);
|
||||
assert.match(community, /<button[^>]*class="graph-group-enter"[^>]*>进入社区<\/button>/);
|
||||
assert.match(community, /<button[^>]*class="graph-group-action"[^>]*data-group-drawer="action"[^>]*>总结这一簇<\/button>/);
|
||||
assert.match(excluded, /<button[^>]*class="graph-summary-action"[^>]*>显示这个对象<\/button>/);
|
||||
assert.doesNotMatch(`${node}${community}${excluded}`, /tabindex="-1"/i);
|
||||
});
|
||||
|
||||
it("keeps the Paper summary styling contract", () => {
|
||||
const css = readFileSync(resolve(import.meta.dirname, "../src/index.css"), "utf8");
|
||||
|
||||
assert.match(css, /\.graph-summary-drawer[\s\S]*border-radius:\s*16px/);
|
||||
assert.match(css, /\.graph-summary-drawer[\s\S]*var\(--paper-grain\)/);
|
||||
assert.match(css, /\.graph-summary-drawer[\s\S]*box-shadow:\s*var\(--shadow-lg\)/);
|
||||
assert.match(css, /\.graph-summary-community-chip::before[\s\S]*width:\s*7px/);
|
||||
assert.match(css, /\.graph-summary-relation-pill[\s\S]*border-radius:\s*999px/);
|
||||
assert.match(css, /\.graph-summary-action[\s\S]*box-shadow:\s*var\(--shadow\)/);
|
||||
assert.doesNotMatch(css, /--app-shadow-color/);
|
||||
assert.doesNotMatch(css, /summary-left-rail|ai-slop/);
|
||||
});
|
||||
|
||||
it("keeps the graph group drawer visual contract", () => {
|
||||
const css = readFileSync(resolve(import.meta.dirname, "../src/index.css"), "utf8");
|
||||
|
||||
assert.match(css, /\.graph-group-node-toggle[\s\S]*color:\s*var\(--app-accent-deep\)/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-facts[\s\S]*grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\)/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact[\s\S]*display:\s*inline-flex/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact[\s\S]*align-items:\s*center/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact[\s\S]*justify-content:\s*center/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact[\s\S]*overflow:\s*hidden/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact[\s\S]*white-space:\s*nowrap/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact strong,[\s\S]*\.graph-group-drawer \.graph-summary-fact span[\s\S]*display:\s*inline-flex/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact strong[\s\S]*text-overflow:\s*ellipsis/);
|
||||
assert.match(css, /\.graph-group-drawer \.graph-summary-fact span[\s\S]*flex:\s*0 0 auto/);
|
||||
assert.match(css, /\.graph-group-status-chip::before[\s\S]*background:\s*var\(--app-accent\)/);
|
||||
assert.match(css, /\.graph-group-enter[\s\S]*border-radius:\s*999px/);
|
||||
assert.match(css, /\.graph-group-action\[data-recommended="true"\][\s\S]*background:\s*color-mix\(in srgb, var\(--app-accent-soft\) 72%, var\(--app-raised\)\)/);
|
||||
assert.doesNotMatch(css, /\.graph-group-action\[data-recommended="true"\][\s\S]{0,260}background:\s*var\(--app-accent\)/);
|
||||
assert.doesNotMatch(css, /\.graph-group-enter[\s\S]{0,260}background:\s*var\(--app-accent\)/);
|
||||
assert.match(css, /\.graph-group-drawer[\s\S]*container-type:\s*inline-size/);
|
||||
assert.match(css, /@container \(max-width:\s*380px\)[\s\S]*\.graph-group-hero-row[\s\S]*grid-template-columns:\s*1fr/);
|
||||
assert.match(css, /\.graph-group-node:hover[\s\S]*border-color:\s*color-mix\(in srgb, var\(--app-accent\)/);
|
||||
assert.match(css, /\.graph-group-node:focus-visible[\s\S]*(box-shadow|outline)/);
|
||||
assert.match(css, /\.graph-selection-context-hint[\s\S]*color:\s*var\(--app-muted\)/);
|
||||
assert.match(css, /\.graph-selection-context-hint span[\s\S]*background:\s*var\(--app-success\)/);
|
||||
assert.match(css, /\.graph-selection-footer[\s\S]*grid-template-columns:/);
|
||||
assert.match(css, /\.graph-selection-footer[\s\S]*minmax\(0,\s*1fr\)/);
|
||||
assert.match(css, /\.graph-selection-send[\s\S]*background:\s*var\(--app-accent\)/);
|
||||
assert.match(css, /\.graph-selection-send svg[\s\S]*width:\s*13px/);
|
||||
assert.match(css, /\.graph-selection-secondary[\s\S]*background:\s*var\(--app-raised\)/);
|
||||
assert.match(css, /\.graph-selection-send:hover:not\(:disabled\)|\.graph-selection-send:focus-visible/);
|
||||
assert.doesNotMatch(css, /搜索命中明细|桥接关系列表|固定节点明细/);
|
||||
});
|
||||
});
|
||||
|
||||
function renderDrawer(drawer: DrawerState): string {
|
||||
return renderToStaticMarkup(
|
||||
React.createElement(RightDrawer, {
|
||||
drawer,
|
||||
fullscreen: false,
|
||||
width: 420,
|
||||
defaultWidth: 420,
|
||||
onSelectArtifact: noopString,
|
||||
onOpenPage: noopString,
|
||||
onWikiLinkSeen: noopString,
|
||||
onGraphReaderAction: noopString,
|
||||
onGraphSummaryCommand: noopCommand,
|
||||
onGraphSummaryNodePreview: noopPreviewNode,
|
||||
onGraphSelectionTextChange: noopString,
|
||||
onGraphSelectionAsk: noopSelectionAsk,
|
||||
onGraphCommunityTextChange: noopString,
|
||||
onGraphCommunityAsk: noopSelectionAsk,
|
||||
onResize: noopNumber,
|
||||
onToggleFullscreen: noop,
|
||||
onClose: noopClose,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function nodeSummaryFixture(): GraphNodeSummaryPayload {
|
||||
return {
|
||||
kind: "node-summary",
|
||||
object: { kind: "node", nodeId: "alpha-node" },
|
||||
nodeId: "alpha-node",
|
||||
label: "Alpha node",
|
||||
type: "topic",
|
||||
communityId: "alpha",
|
||||
sourcePath: "wiki/alpha.md",
|
||||
summary: "short excerpt only",
|
||||
connectionCount: 3,
|
||||
searchHit: true,
|
||||
pinHint: { nodeId: "alpha-node", wikiPath: "wiki/alpha.md", pinned: false, position: null },
|
||||
selection: {
|
||||
input: { kind: "node", id: "alpha-node" },
|
||||
selectionId: "node:alpha-node",
|
||||
selectedNodeIds: ["alpha-node"],
|
||||
selectedCommunityIds: ["alpha"],
|
||||
containsCurrentObject: true,
|
||||
},
|
||||
strongestRelations: [
|
||||
{
|
||||
edgeId: "alpha-beta",
|
||||
fromNodeId: "alpha-node",
|
||||
toNodeId: "beta-node",
|
||||
relationType: "依赖",
|
||||
confidence: "EXTRACTED",
|
||||
weight: 1,
|
||||
bridge: false,
|
||||
},
|
||||
],
|
||||
bridgeRelations: [],
|
||||
aggregationMarkers: [],
|
||||
commands: [
|
||||
{ kind: "open-detail-read", nodeId: "alpha-node", path: "wiki/alpha.md", label: "打开详情" },
|
||||
{ kind: "select-neighbors", nodeId: "alpha-node", label: "+邻居" },
|
||||
{ kind: "set-fixed-position", mode: "fix", nodeId: "alpha-node", wikiPath: "wiki/alpha.md", label: "固定位置" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function graphPayload() {
|
||||
return {
|
||||
path: "wiki/alpha.md",
|
||||
node: {
|
||||
id: "alpha-node",
|
||||
title: "Alpha node",
|
||||
type: "topic",
|
||||
typeLabel: "主题",
|
||||
sourcePath: "wiki/alpha.md",
|
||||
community: "alpha",
|
||||
date: "2026-06-18",
|
||||
source: "fixture",
|
||||
isolated: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function communitySummaryFixture(overrides: Partial<GraphCommunitySummaryPayload> = {}): GraphCommunitySummaryPayload {
|
||||
return {
|
||||
kind: "community-summary",
|
||||
object: { kind: "community", communityId: "alpha" },
|
||||
communityId: "alpha",
|
||||
label: "Alpha community",
|
||||
nodeCount: 12,
|
||||
facts: { pageCount: 12, internalLinkCount: 8, communityCount: 1, isolatedCount: 1 },
|
||||
structureState: "clear",
|
||||
description: "这组页面围绕同一主题聚在一起。你可以先看结构,也可以直接让 agent 基于这一组页面继续工作。",
|
||||
canEnterCommunity: true,
|
||||
coreNodeIds: ["alpha-node", "beta-node", "gamma-node", "delta-node"],
|
||||
coreNodes: [
|
||||
{ nodeId: "alpha-node", label: "Alpha node", type: "topic", role: "核心" },
|
||||
{ nodeId: "beta-node", label: "Beta node", type: "entity", role: "相关" },
|
||||
{ nodeId: "gamma-node", label: "Gamma node", type: "source", role: "相关" },
|
||||
{ nodeId: "delta-node", label: "Delta node", type: "entity", role: "相关" },
|
||||
],
|
||||
searchResultIds: ["beta-node"],
|
||||
pinHints: [
|
||||
{ nodeId: "gamma-node", wikiPath: "wiki/gamma.md", pinned: true, position: { x: 12, y: 18, coordinateSpace: "world" } },
|
||||
],
|
||||
selection: {
|
||||
input: { kind: "community", id: "alpha" },
|
||||
selectionId: "community:alpha",
|
||||
selectedNodeIds: ["alpha-node", "beta-node"],
|
||||
selectedCommunityIds: ["alpha"],
|
||||
containsCurrentObject: true,
|
||||
},
|
||||
strongestRelations: [],
|
||||
bridgeRelations: [],
|
||||
aggregationMarkers: [],
|
||||
commands: [
|
||||
{ kind: "enter-community", communityId: "alpha", label: "进入社区" },
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function unavailableFixture(): GraphUnavailableObjectPayload {
|
||||
return {
|
||||
kind: "unavailable-object",
|
||||
object: { kind: "node", nodeId: "missing-node" },
|
||||
reason: "missing-node",
|
||||
selection: {
|
||||
input: null,
|
||||
selectionId: null,
|
||||
selectedNodeIds: [],
|
||||
selectedCommunityIds: [],
|
||||
containsCurrentObject: false,
|
||||
},
|
||||
searchResultIds: [],
|
||||
pinHints: [],
|
||||
aggregationMarkers: [],
|
||||
commands: [],
|
||||
};
|
||||
}
|
||||
|
||||
function excludedFixture(): GraphExcludedObjectPayload {
|
||||
return {
|
||||
kind: "excluded-object",
|
||||
object: { kind: "node", nodeId: "filtered-node" },
|
||||
reason: "filter",
|
||||
selection: {
|
||||
input: { kind: "node", id: "filtered-node" },
|
||||
selectionId: "node:filtered-node",
|
||||
selectedNodeIds: ["filtered-node"],
|
||||
selectedCommunityIds: ["alpha"],
|
||||
containsCurrentObject: true,
|
||||
},
|
||||
searchResultIds: [],
|
||||
pinHints: [],
|
||||
aggregationMarkers: [],
|
||||
commands: [
|
||||
{ kind: "show-this-object", object: { kind: "node", nodeId: "filtered-node" }, label: "显示这个对象" },
|
||||
{ kind: "clear-temporary-object-display", label: "清除临时显示" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
function noopString() {}
|
||||
function noopNumber() {}
|
||||
function noopCommand() {}
|
||||
function noopPreviewNode() {}
|
||||
function noopSelectionAsk() {}
|
||||
function noopClose() {}
|
||||
Reference in New Issue
Block a user