1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Refactor and harden graph browser assets

This commit is contained in:
Andraxion 2026-07-25 16:46:00 -04:00
parent 7a940b60c1
commit f9f7105983
14 changed files with 1955 additions and 1867 deletions

View file

@ -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"])