This commit is contained in:
2026-07-12 21:26:08 +08:00
commit 9dd41afd48
502 changed files with 129901 additions and 0 deletions
@@ -0,0 +1,200 @@
import { afterEach, 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 { GraphPanel } from "../src/components/GraphPanel";
import { click, pressKey, render, screen, waitFor } from "./render";
describe("GraphPanel Paper shell", () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
it("renders the graph shell toolbar without adding app-level graph overlays", async () => {
mockGraphFetch();
render(
<GraphPanel
currentKnowledgeBaseName="AI 学习库"
currentKnowledgeBasePath="/kb"
theme="light"
/>,
);
const toolbar = screen.getByRole("banner", { name: "图谱工具栏" });
assert.equal(toolbar.classList.contains("graph-shell-toolbar"), true);
assert.match(toolbar.textContent ?? "", /图谱活地图/);
assert.ok(screen.getByRole("button", { name: /重置布局/ }));
assert.ok(screen.getByRole("button", { name: /调参/ }));
assert.ok(screen.getByRole("button", { name: /重构/ }));
assert.notEqual(toolbar.querySelector(".graph-shell-legend"), null);
await waitFor(() => {
assert.match(toolbar.textContent ?? "", /1 节点 · 0 关联/);
});
const shell = document.querySelector(".graph-shell");
const stage = document.querySelector(".graph-stage");
assert.ok(shell);
assert.ok(stage);
assert.equal(shell?.contains(toolbar), true);
assert.equal(shell?.contains(stage), true);
assert.equal(stage?.contains(toolbar), false);
assert.notEqual(screen.queryByLabelText("图谱图例"), null);
assert.equal(document.querySelector(".graph-legend"), null);
assert.ok(document.querySelector(".graph-host"));
});
it("opens graph edge tuning controls from the toolbar", async () => {
mockGraphFetch();
render(
<GraphPanel
currentKnowledgeBaseName="AI 学习库"
currentKnowledgeBasePath="/kb"
theme="light"
/>,
);
const tuningButton = screen.getByRole("button", { name: /调参/ }) as HTMLButtonElement;
await waitFor(() => {
assert.equal(tuningButton.disabled, false);
});
await click(tuningButton);
const panel = screen.getByRole("dialog", { name: "图谱调参" });
assert.match(panel.textContent ?? "", /分主次/);
const semanticToggle = screen.getByRole("checkbox", { name: "语义强调" });
const focusToggle = screen.getByRole("checkbox", { name: "聚焦点亮" });
assert.equal((semanticToggle as HTMLInputElement).checked, false);
assert.equal((focusToggle as HTMLInputElement).checked, false);
assert.equal(document.activeElement, semanticToggle);
await click(semanticToggle);
await waitFor(() => {
assert.equal((semanticToggle as HTMLInputElement).checked, true);
assert.equal(localStorage.getItem("llm-wiki.graph.edge-style"), "{\"semanticEmphasis\":true,\"focusHighlight\":false}");
});
await click(focusToggle);
await waitFor(() => {
assert.equal((focusToggle as HTMLInputElement).checked, true);
assert.equal(localStorage.getItem("llm-wiki.graph.edge-style"), "{\"semanticEmphasis\":true,\"focusHighlight\":true}");
});
await pressKey(document, "Escape");
assert.equal(screen.queryByRole("dialog", { name: "图谱调参" }), null);
assert.equal(document.activeElement, tuningButton);
});
it("surfaces graph build errors in the graph panel", async () => {
mockGraphFetch({ needsBuild: true });
const { rerender } = render(
<GraphPanel
currentKnowledgeBaseName="AI 学习库"
currentKnowledgeBasePath="/kb"
theme="light"
/>,
);
await waitFor(() => {
assert.match(screen.getByText("图谱构建中").textContent ?? "", /图谱构建中/);
});
rerender(
<GraphPanel
currentKnowledgeBaseName="AI 学习库"
currentKnowledgeBasePath="/kb"
theme="light"
graphBuildError={{ kbPath: "/kb", message: "构建失败", rebuiltAt: "2026-06-20T00:00:00.000Z" }}
/>,
);
await waitFor(() => {
assert.match(screen.getByText("图谱暂时不可用").textContent ?? "", /图谱暂时不可用/);
assert.match(document.body.textContent ?? "", /构建失败/);
});
});
it("keeps the GraphPanel Paper shell styling outside graph-engine internals", () => {
const css = readFileSync(resolve(import.meta.dirname, "../src/index.css"), "utf8");
assert.match(css, /\.graph-shell\s*\{[\s\S]*display:\s*grid/);
assert.match(css, /\.graph-shell-toolbar[\s\S]*position:\s*relative/);
assert.match(css, /\.graph-stage[\s\S]*min-height:\s*0/);
assert.match(css, /\.graph-shell-legend[\s\S]*display:\s*flex/);
assert.doesNotMatch(cssBlock(css, ".graph-shell-toolbar"), /position:\s*absolute/);
assert.match(css, /\.graph-shell-toolbar[\s\S]*var\(--paper-grain\)/);
assert.match(css, /\.graph-shell-toolbar-chip,[\s\S]*\.graph-shell-toolbar-button/);
assert.match(css, /\.graph-stage[\s\S]*border-radius:\s*16px/);
assert.doesNotMatch(css, /(^|\n)\s*\.graph-toolbar\b/);
assert.doesNotMatch(css, /(^|\n)\s*\.graph-legend\b/);
assert.doesNotMatch(css, /render-styles|sigma-node|sigma-edge/);
});
});
function cssBlock(css: string, selector: string): string {
const start = css.indexOf(`${selector} {`);
if (start === -1) return "";
const end = css.indexOf("\n }", start);
return end === -1 ? css.slice(start) : css.slice(start, end);
}
function mockGraphFetch(options: { needsBuild?: boolean } = {}) {
globalThis.fetch = (async (input) => {
const url = String(input);
if (url.startsWith("/api/graph/layout")) {
return jsonResponse({
ok: true,
layout: { version: 1, pins: {}, updatedAt: "2026-06-20T00:00:00.000Z" },
});
}
if (url.startsWith("/api/graph?")) {
if (options.needsBuild) {
return jsonResponse({
ok: true,
needsBuild: true,
graphPath: "/kb/.llm-wiki/graph.json",
});
}
return jsonResponse({
ok: true,
needsBuild: false,
data: {
meta: { build_date: "2026-06-20T00:00:00.000Z" },
nodes: [
{
id: "wiki/paper.md",
label: "Paper",
title: "Paper",
type: "topic",
community: "paper",
path: "wiki/paper.md",
},
],
edges: [],
communities: [],
},
});
}
if (url.startsWith("/api/graph/rebuild")) {
return jsonResponse({ ok: true, status: "started" });
}
return jsonResponse({ ok: false, error: `Unexpected request: ${url}` }, 500);
}) as typeof fetch;
}
function jsonResponse(payload: unknown, status = 200) {
return new Response(JSON.stringify(payload), {
status,
headers: { "Content-Type": "application/json" },
});
}