mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
e4da76979e
Symbol view (full rebuild)
──────────────────────────
Replaces the unusable "show all symbols at once" mode with a focus
graph (SourceTrail / IntelliJ pattern):
- Landing state shows the alphabetical list of all symbols in the
sidebar; canvas shows a "pick a symbol" overlay constrained to the
canvas area (right: 340px) so the list stays visible.
- Three entry paths to seed: list click, search bar, or symbol click
from a file's sidebar in Files view.
- Once seeded, canvas shows the symbol + its 2-hop callers/callees
neighbourhood (auto-falls back to depth 1 if > 60 nodes).
- Clicking any neighbour re-centres on it. Seed has a distinctive
orange ring + always-visible label so the anchor is obvious.
- "← Back to symbol list" link in the sidebar restores the empty
state — explicit way out instead of relying on accidental
empty-canvas clicks.
- Light file-grouping: each symbol's border colour is a stable hash
of its file path; symbols from the same file share a colour.
Layout dropdown clean-up
────────────────────────
Removed Force-directed (cose). Force-directed is the wrong algorithm
for code dependency graphs — hub clusters collapse, orphans fly off,
labels overlap, no tuning fixes the underlying shape mismatch. Tried
fcose as a substitute; same fundamental problem on dense code graphs.
Default is now Dagre TB; remaining options are Concentric, Breadth-
first, Grid, Circle — all deterministic.
UX polish
─────────
- autoungrabify: true — disables single-click drag, fixes "node jumps
on click" (trackpad clicks always have some motion which Cytoscape's
default interprets as a node grab).
- Tap-to-highlight neighbourhood — clicking any node highlights its
direct neighbours and fades the rest. Less aggressive than the
transitive blast-radius / call-flow buttons in the sidebar.
- Zoom-bound label visibility — labels hidden below zoom 0.55 so 100+
node graphs aren't a soup of overlapping text. Selected / highlighted
nodes always show their label.
- Layout-position persistence — switching Files ↔ Symbols and back
preserves Files-view positions instead of re-running the layout.
TDZ regression test
───────────────────
New tests/unit/viewer-app.test.ts runs the bundled viewer-app.js in a
sandboxed node:vm context with mocked DOM + Cytoscape, triggering
every function-typed style closure on a fake element. Catches TDZ
("Cannot access X before initialization") and other reference errors
at unit-test time. Would have caught both the prior `cy` and
`LABEL_ZOOM_THRESHOLD` ordering bugs before they reached the browser.
Stats-line consistency
──────────────────────
graph-visualize-html.ts — top bar now displays embedded counts
(sym.symbols.length / sym.symbolEdges.length) instead of meta counts.
Previously the top bar said "630 symbols" while the sidebar's "All
symbols" said "722" because meta excludes synthetic <module>
placeholders. Capped mode keeps meta counts with a "(capped)"
qualifier since the embedded set is empty there.
Quality gates
─────────────
- Biome lint: clean
- TypeScript (tsc): clean
- Unit tests: 687/687 (incl. the new viewer-app.js evaluation test)
125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
|
|
|
|
/**
|
|
* Regression test for the bundled viewer-app.js — runs it in a mocked
|
|
* DOM/Cytoscape environment and asserts it loads without throwing.
|
|
*
|
|
* This catches TDZ ("Cannot access 'X' before initialization") errors
|
|
* introduced by declaring a `const`/`let` after it's used inside a style
|
|
* closure that Cytoscape evaluates during the `cy = cytoscape({...})`
|
|
* constructor call. The standard unit tests on `graph-visualize-html.ts`
|
|
* check the generated HTML's structure but never execute the script, so
|
|
* runtime errors like that only surface when a browser loads the page.
|
|
*
|
|
* Uses Node's `vm` module for a sandboxed evaluation — the viewer source
|
|
* is trusted (we ship it ourselves) but isolating the globals keeps the
|
|
* test safe and deterministic.
|
|
*/
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { runInNewContext } from "node:vm";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const VIEWER_JS_PATH = path.resolve(__dirname, "../../src/assets/viewer-app.js");
|
|
|
|
describe("viewer-app.js", () => {
|
|
it("executes top-to-bottom without TDZ or reference errors", () => {
|
|
const src = readFileSync(VIEWER_JS_PATH, "utf-8");
|
|
|
|
// ── Mock DOM ────────────────────────────────────────────────────
|
|
const makeEl = (id: string) => ({
|
|
id,
|
|
className: "",
|
|
classList: { add: () => {}, remove: () => {}, toggle: () => {} },
|
|
style: {},
|
|
disabled: false,
|
|
title: "",
|
|
textContent: "",
|
|
firstChild: null as null | object,
|
|
dataset: {} as Record<string, string>,
|
|
addEventListener: () => {},
|
|
appendChild: () => {},
|
|
removeChild: () => {},
|
|
setAttribute: () => {},
|
|
removeAttribute: () => {},
|
|
});
|
|
const elements = new Map<string, ReturnType<typeof makeEl>>();
|
|
const doc = {
|
|
createElement: () => makeEl("created"),
|
|
createTextNode: () => ({}),
|
|
getElementById: (id: string) => {
|
|
if (!elements.has(id)) elements.set(id, makeEl(id));
|
|
return elements.get(id);
|
|
},
|
|
querySelectorAll: () => [] as unknown[],
|
|
};
|
|
|
|
// ── Mock Cytoscape — exercises every style closure on a fake ele ──
|
|
const closureErrors: string[] = [];
|
|
const makeFakeCy = () => ({
|
|
on: () => {},
|
|
zoom: () => 1,
|
|
width: () => 800,
|
|
height: () => 600,
|
|
elements: () => ({
|
|
remove: () => {},
|
|
addClass: () => {},
|
|
removeClass: () => {},
|
|
difference: () => ({
|
|
addClass: () => {},
|
|
nodes: () => ({ difference: () => ({ addClass: () => {} }) }),
|
|
edges: () => ({ addClass: () => {} }),
|
|
}),
|
|
}),
|
|
nodes: () => ({ forEach: () => {}, length: 0, filter: () => [] as unknown[] }),
|
|
getElementById: () => ({ empty: () => true }),
|
|
add: () => {},
|
|
fit: () => {},
|
|
style: () => ({ update: () => {} }),
|
|
layout: () => ({ one: () => {}, run: () => {} }),
|
|
batch: (fn: () => void) => fn(),
|
|
png: () => "data:,",
|
|
animate: () => {},
|
|
});
|
|
|
|
const fakeCy = (config: { style?: Array<{ selector: string; style?: Record<string, unknown> }> }) => {
|
|
const instance = makeFakeCy();
|
|
const fakeEle = { data: () => "x", hasClass: () => false, cy: () => instance };
|
|
// Exercise every function-typed style value. Any TDZ or reference
|
|
// error surfaces here as a collected closureErrors entry.
|
|
for (const rule of config.style ?? []) {
|
|
for (const [key, val] of Object.entries(rule.style ?? {})) {
|
|
if (typeof val === "function") {
|
|
try { (val as (ele: unknown) => unknown)(fakeEle); }
|
|
catch (e) { closureErrors.push(`${rule.selector} → ${key}: ${(e as Error).message}`); }
|
|
}
|
|
}
|
|
}
|
|
return instance;
|
|
};
|
|
|
|
const data = {
|
|
project: { name: "smoke" },
|
|
files: [{ id: "a.ts", label: "a.ts", language: "typescript", deps: 0, dependents: 0, symbolCount: 0 }],
|
|
fileEdges: [],
|
|
symbols: [],
|
|
symbolEdges: [],
|
|
symbolsByFile: {},
|
|
symbolMode: "full" as const,
|
|
};
|
|
|
|
const ctx = {
|
|
document: doc,
|
|
cytoscape: fakeCy,
|
|
window: { __SOCRATICODE_DATA__: data, cytoscapeDagre: () => {} },
|
|
console: { log: () => {}, warn: () => {}, error: () => {} },
|
|
};
|
|
|
|
// Throws if the viewer hits TDZ or any other top-level error.
|
|
expect(() => runInNewContext(src, ctx, { filename: "viewer-app.js" })).not.toThrow();
|
|
expect(closureErrors).toEqual([]);
|
|
});
|
|
});
|