Refactor and harden graph browser assets
This commit is contained in:
parent
7a940b60c1
commit
f9f7105983
14 changed files with 1955 additions and 1867 deletions
|
|
@ -131,7 +131,7 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
|||
visualization = results[11].structuredContent["visualization"]
|
||||
self.assertTrue(visualization["read_only"])
|
||||
self.assertTrue(visualization["project_bound"])
|
||||
self.assertEqual("graph-browser@12", visualization["template"])
|
||||
self.assertEqual("graph-browser@13", visualization["template"])
|
||||
self.assertEqual("managed_idle", visualization["lifetime"]["policy"])
|
||||
self.assertEqual("docforge_stop_visualization", visualization["lifetime"]["stop_tool"])
|
||||
self.assertTrue(visualization["url"].startswith("http://127.0.0.1:"))
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ from docforge.index import ProjectIndex
|
|||
from docforge.project import Project
|
||||
from docforge.viewer_manager import ViewerManager, ViewerManagerClient
|
||||
from docforge.visualization import (
|
||||
_GRAPH_BROWSER_CSS,
|
||||
_GRAPH_BROWSER_HTML,
|
||||
_GRAPH_BROWSER_JAVASCRIPT,
|
||||
VISUALIZATION_TEMPLATE,
|
||||
VisualizationIndexSnapshot,
|
||||
VisualizationRunner,
|
||||
|
|
@ -100,12 +102,11 @@ class VisualizationTests(unittest.TestCase):
|
|||
with self.assertRaisesRegex(DocForgeError, "category is unsupported"):
|
||||
snapshot.filter_nodes(category="relation", value="depends_on", limit=2)
|
||||
|
||||
@unittest.skipUnless(shutil.which("node"), "Node.js is required for embedded script validation")
|
||||
def test_embedded_browser_javascript_is_valid(self) -> None:
|
||||
script = _GRAPH_BROWSER_HTML.split("<script>", 1)[1].split("</script>", 1)[0]
|
||||
@unittest.skipUnless(shutil.which("node"), "Node.js is required for browser script validation")
|
||||
def test_browser_javascript_is_valid(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "graph-browser.js"
|
||||
path.write_text(script, encoding="utf-8")
|
||||
path.write_text(_GRAPH_BROWSER_JAVASCRIPT, encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
["node", "--check", str(path)],
|
||||
check=False,
|
||||
|
|
@ -120,12 +121,15 @@ class VisualizationTests(unittest.TestCase):
|
|||
self.assertIn('id="open-node-source"', _GRAPH_BROWSER_HTML)
|
||||
self.assertIn('id="hide-node"', _GRAPH_BROWSER_HTML)
|
||||
self.assertIn('id="source-dialog"', _GRAPH_BROWSER_HTML)
|
||||
self.assertIn("state.hiddenNodes.add(nodeId)", _GRAPH_BROWSER_HTML)
|
||||
self.assertIn("grid-template-rows: auto minmax(0, 1fr) auto", _GRAPH_BROWSER_HTML)
|
||||
self.assertIn("state.hiddenNodes.add(nodeId)", _GRAPH_BROWSER_JAVASCRIPT)
|
||||
self.assertIn(
|
||||
"grid-template-rows: auto minmax(0, 1fr) auto",
|
||||
_GRAPH_BROWSER_CSS,
|
||||
)
|
||||
|
||||
@unittest.skipUnless(shutil.which("node"), "Node.js is required for topology validation")
|
||||
def test_embedded_topology_roles_hops_and_shading_are_deterministic(self) -> None:
|
||||
script = _GRAPH_BROWSER_HTML.split("<script>", 1)[1].split("</script>", 1)[0]
|
||||
def test_topology_roles_hops_and_shading_are_deterministic(self) -> None:
|
||||
script = _GRAPH_BROWSER_JAVASCRIPT
|
||||
viewport_logic = script.split("function viewportForPositions", 1)[1].split(
|
||||
"function selectNode", 1
|
||||
)[0]
|
||||
|
|
@ -255,7 +259,18 @@ if (!containmentEdge || containmentEdge.target_id !== "primary") {
|
|||
with urllib.request.urlopen(base, timeout=2) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
headers = response.headers
|
||||
with urllib.request.urlopen(f"{base}assets/graph.css", timeout=2) as response:
|
||||
css = response.read().decode("utf-8")
|
||||
self.assertEqual("text/css; charset=utf-8", response.headers["Content-Type"])
|
||||
with urllib.request.urlopen(f"{base}assets/graph.js", timeout=2) as response:
|
||||
javascript = response.read().decode("utf-8")
|
||||
self.assertEqual(
|
||||
"text/javascript; charset=utf-8",
|
||||
response.headers["Content-Type"],
|
||||
)
|
||||
self.assertIn("DocForge graph", html)
|
||||
self.assertIn('href="assets/graph.css"', html)
|
||||
self.assertIn('src="assets/graph.js"', html)
|
||||
self.assertIn('id="zoom-in"', html)
|
||||
self.assertIn('id="zoom-out"', html)
|
||||
self.assertIn('id="reset-view"', html)
|
||||
|
|
@ -263,15 +278,16 @@ if (!containmentEdge || containmentEdge.target_id !== "primary") {
|
|||
self.assertIn('id="node-card"', html)
|
||||
self.assertIn('id="explore-node"', html)
|
||||
self.assertIn('id="explore-card-node"', html)
|
||||
self.assertIn("positionNodeCard(dialog, event)", html)
|
||||
self.assertIn("positionNodeCard(dialog, event)", javascript)
|
||||
self.assertIn(
|
||||
'renderDetails($("node-card-details"), data.node, data, true, false)', html
|
||||
'renderDetails($("node-card-details"), data.node, data, true, false)',
|
||||
javascript,
|
||||
)
|
||||
self.assertIn("dialog.show();", html)
|
||||
self.assertIn("position: fixed; inset: auto; margin: 0", html)
|
||||
self.assertIn("background: rgba(7, 18, 29, .82)", html)
|
||||
self.assertIn("width: fit-content; height: fit-content", html)
|
||||
self.assertIn("max-width: min(760px, calc(100vw - 32px))", html)
|
||||
self.assertIn("dialog.show();", javascript)
|
||||
self.assertIn("position: fixed; inset: auto; margin: 0", css)
|
||||
self.assertIn("background: rgba(7, 18, 29, .82)", css)
|
||||
self.assertIn("width: fit-content; height: fit-content", css)
|
||||
self.assertIn("max-width: min(760px, calc(100vw - 32px))", css)
|
||||
self.assertIn('id="left-resizer"', html)
|
||||
self.assertIn('id="right-resizer"', html)
|
||||
self.assertIn('id="view-nodes"', html)
|
||||
|
|
@ -280,53 +296,66 @@ if (!containmentEdge || containmentEdge.target_id !== "primary") {
|
|||
self.assertIn('id="relationship-key"', html)
|
||||
self.assertIn('id="relationship-key-list"', html)
|
||||
self.assertNotIn('id="details"', html)
|
||||
self.assertIn(".empty[hidden] { display: none; }", html)
|
||||
self.assertIn("html, body { height: 100%; overflow: hidden; }", html)
|
||||
self.assertIn(".empty[hidden] { display: none; }", css)
|
||||
self.assertIn("html, body { height: 100%; overflow: hidden; }", css)
|
||||
self.assertIn("Space centers selection", html)
|
||||
self.assertIn("resize: both", html)
|
||||
self.assertNotIn("backdrop-filter", html)
|
||||
self.assertIn('addEventListener("wheel"', html)
|
||||
self.assertIn('addEventListener("pointermove"', html)
|
||||
self.assertIn("inspectNode(node.node_id)", html)
|
||||
self.assertIn("showNodeCard(node.node_id)", html)
|
||||
self.assertIn('addEventListener("contextmenu"', html)
|
||||
self.assertIn("dialog.showModal()", html)
|
||||
self.assertIn("await loadNode(nodeId)", html)
|
||||
self.assertIn("beginDialogDrag", html)
|
||||
self.assertIn('setupPanelResizer("left")', html)
|
||||
self.assertIn('setupPanelResizer("right")', html)
|
||||
self.assertIn("Focus node", html)
|
||||
self.assertIn("Outgoing paths", html)
|
||||
self.assertIn("Incoming & lateral", html)
|
||||
self.assertIn("resize: both", css)
|
||||
self.assertNotIn("backdrop-filter", css)
|
||||
self.assertIn('addEventListener("wheel"', javascript)
|
||||
self.assertIn('addEventListener("pointermove"', javascript)
|
||||
self.assertIn("inspectNode(node.node_id)", javascript)
|
||||
self.assertIn("showNodeCard(node.node_id)", javascript)
|
||||
self.assertIn('addEventListener("contextmenu"', javascript)
|
||||
self.assertIn("dialog.showModal()", javascript)
|
||||
self.assertIn("await loadNode(nodeId)", javascript)
|
||||
self.assertIn("beginDialogDrag", javascript)
|
||||
self.assertIn('setupPanelResizer("left")', javascript)
|
||||
self.assertIn('setupPanelResizer("right")', javascript)
|
||||
self.assertIn("Focus node", javascript)
|
||||
self.assertIn("Outgoing paths", javascript)
|
||||
self.assertIn("Incoming & lateral", javascript)
|
||||
self.assertNotIn(">Children<", html)
|
||||
self.assertIn("distanceShade", html)
|
||||
self.assertIn("viewportForPositions", html)
|
||||
self.assertIn("centerSelectedNode", html)
|
||||
self.assertIn("relationStyles", html)
|
||||
self.assertIn("appendRelationMarker", html)
|
||||
self.assertIn("renderRelationshipKey", html)
|
||||
self.assertIn("buildFlowGraph", html)
|
||||
self.assertIn("layoutFlow", html)
|
||||
self.assertIn('api(`${showingFlow ? "lineage" : "node"}?${params}`)', html)
|
||||
self.assertIn("filterByDescriptor", html)
|
||||
self.assertIn("api(`filter?${params}`)", html)
|
||||
self.assertIn('setViewMode("flow")', html)
|
||||
self.assertIn('event.code !== "Space"', html)
|
||||
self.assertIn('class: "selection-ring"', html)
|
||||
self.assertIn("renewViewerLease", html)
|
||||
pointerdown = html.split('$("graph").addEventListener("pointerdown"', 1)[1].split(
|
||||
'$("graph").addEventListener("pointermove"', 1
|
||||
)[0]
|
||||
pointermove = html.split('$("graph").addEventListener("pointermove"', 1)[1].split(
|
||||
"function endPan", 1
|
||||
)[0]
|
||||
self.assertIn("distanceShade", javascript)
|
||||
self.assertIn("viewportForPositions", javascript)
|
||||
self.assertIn("centerSelectedNode", javascript)
|
||||
self.assertIn("relationStyles", javascript)
|
||||
self.assertIn("appendRelationMarker", javascript)
|
||||
self.assertIn("renderRelationshipKey", javascript)
|
||||
self.assertIn("buildFlowGraph", javascript)
|
||||
self.assertIn("layoutFlow", javascript)
|
||||
self.assertIn(
|
||||
'api(`${showingFlow ? "lineage" : "node"}?${params}`)',
|
||||
javascript,
|
||||
)
|
||||
self.assertIn("filterByDescriptor", javascript)
|
||||
self.assertIn("api(`filter?${params}`)", javascript)
|
||||
self.assertIn('setViewMode("flow")', javascript)
|
||||
self.assertIn('event.code !== "Space"', javascript)
|
||||
self.assertIn('class: "selection-ring"', javascript)
|
||||
self.assertIn("renewViewerLease", javascript)
|
||||
pointerdown = javascript.split(
|
||||
'$("graph").addEventListener("pointerdown"',
|
||||
1,
|
||||
)[1].split('$("graph").addEventListener("pointermove"', 1)[0]
|
||||
pointermove = javascript.split(
|
||||
'$("graph").addEventListener("pointermove"',
|
||||
1,
|
||||
)[1].split("function endPan", 1)[0]
|
||||
self.assertNotIn("setPointerCapture", pointerdown)
|
||||
self.assertIn("setPointerCapture", pointermove)
|
||||
self.assertIn("right-click full inspector", html)
|
||||
self.assertIn("default-src 'none'", headers["Content-Security-Policy"])
|
||||
self.assertIn("script-src 'self'", headers["Content-Security-Policy"])
|
||||
self.assertIn("style-src 'self'", headers["Content-Security-Policy"])
|
||||
self.assertNotIn("unsafe-inline", headers["Content-Security-Policy"])
|
||||
self.assertEqual("no-store", headers["Cache-Control"])
|
||||
self.assertEqual("DENY", headers["X-Frame-Options"])
|
||||
|
||||
with self.assertRaises(urllib.error.HTTPError) as missing_asset:
|
||||
urllib.request.urlopen(f"{base}assets/missing.js", timeout=2)
|
||||
self.assertEqual(404, missing_asset.exception.code)
|
||||
missing_asset.exception.close()
|
||||
|
||||
with urllib.request.urlopen(f"{base}api/overview", timeout=2) as response:
|
||||
overview = json.load(response)
|
||||
self.assertEqual("alpha-docs", overview["project_id"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue