mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-12 04:01:35 +00:00
* fix(explorer): label the graph view control "Focus" The control read "Focused" before it was ever activated, which describes a state the selection had already reached rather than the action available. The view mode value, tooltip, enablement and active styling are unchanged. The legend e2e drove this button by its accessible name, so the selector moves with the label; it now also asserts the visible text, the selection-dependent enablement and the active state. Closes #1551 Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(explorer): name the control in the grouped-selection hint, and trim the test "Activate Focused mode" instructed the reader to press a control that no longer carries that name. The surrounding strings describe the mode itself, which is still called focused, so they stay. Drop the label and active-state assertions from the colour-legend test: the getByRole locator already fails when the accessible name is wrong, and the rest belonged to the control's contract rather than to the legend's. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
115 lines
6.0 KiB
TypeScript
115 lines
6.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import test from "node:test";
|
|
import { chromium, type Page } from "playwright";
|
|
|
|
const BASE_URL = "http://127.0.0.1:4175";
|
|
const initialNodes = [
|
|
{ id: "alice", type: "Person", content: "Alice", properties: {} },
|
|
{ id: "bob", type: "Person", content: "Bob", properties: {} },
|
|
{ id: "acme", type: "Organization", content: "Acme", properties: {} },
|
|
{ id: "london", type: "Location", content: "London", properties: {} },
|
|
{ id: "research", type: "Project", content: "Research", properties: {} },
|
|
{ id: "report", type: "Document", content: "Report", properties: {} },
|
|
];
|
|
const edges = [
|
|
["alice", "acme", "WORKS_AT"], ["bob", "acme", "WORKS_AT"],
|
|
["acme", "london", "LOCATED_IN"], ["alice", "research", "LEADS"],
|
|
["bob", "report", "AUTHORED"], ["report", "research", "DESCRIBES"],
|
|
].map(([source, target, type], i) => ({
|
|
id: `edge_${i}`, familyId: `edge_${i}`, source, target, type, weight: 1, properties: {},
|
|
}));
|
|
|
|
async function assertLegendMatchesGraph(page: Page, nodeIds?: string[]) {
|
|
const result = await page.evaluate(async (includedNodeIds) => {
|
|
const storePath = "/src/store/graphStore.ts";
|
|
const { graph } = await import(storePath);
|
|
const colors: Record<string, string> = {};
|
|
graph.forEachNode((id: string, attrs: { semanticGroup: string; baseColor: string }) => {
|
|
if (includedNodeIds && !includedNodeIds.includes(id)) return;
|
|
const hex = attrs.baseColor.replace("#", "");
|
|
colors[attrs.semanticGroup] = `rgb(${[0, 2, 4].map((i) => parseInt(hex.slice(i, i + 2), 16)).join(", ")})`;
|
|
});
|
|
const items = [...document.querySelectorAll(".explore-color-legend-item")].map((item) => ({
|
|
group: item.querySelector(".explore-color-legend-name")?.textContent,
|
|
color: getComputedStyle(item.querySelector(".explore-color-legend-mark")!).backgroundColor,
|
|
}));
|
|
return { colors, items };
|
|
}, nodeIds);
|
|
assert.equal(result.items.length, Object.keys(result.colors).length);
|
|
for (const item of result.items) {
|
|
assert.equal(item.color, result.colors[item.group!], `Swatch for ${item.group} must match the loaded canvas color`);
|
|
}
|
|
}
|
|
|
|
test("visible legend follows loaded data, reloads, focused views, and distance mode", async (t) => {
|
|
const server = spawn("npm", ["run", "dev", "--", "--host", "127.0.0.1", "--port", "4175", "--strictPort"], { stdio: "ignore" });
|
|
t.after(() => { server.kill(); });
|
|
let ready = false;
|
|
for (let i = 0; i < 100; i += 1) {
|
|
try { if ((await fetch(BASE_URL)).ok) { ready = true; break; } } catch { /* Starting Vite. */ }
|
|
await delay(100);
|
|
}
|
|
assert.ok(ready, "Vite must start");
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath:
|
|
process.env.CHROMIUM_PATH || (existsSync("/usr/bin/chromium") ? "/usr/bin/chromium" : undefined),
|
|
});
|
|
t.after(() => browser.close());
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
|
page.setDefaultTimeout(10_000);
|
|
const errors: string[] = [];
|
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); });
|
|
let nodes = initialNodes;
|
|
await page.routeWebSocket("**/ws/graph-updates", () => {});
|
|
await page.route("**/api/**", async (route) => {
|
|
const path = new URL(route.request().url()).pathname;
|
|
let json: unknown = {};
|
|
if (path === "/api/info") json = { capabilities: { agent_memory: false } };
|
|
if (path === "/api/graph/stats") json = { node_count: nodes.length, edge_count: edges.length };
|
|
if (path === "/api/graph/nodes") json = { nodes, total: nodes.length, next_cursor: null };
|
|
if (path === "/api/graph/edges") json = { edges, total: edges.length, next_cursor: null };
|
|
if (path === "/api/temporal/bounds") json = { min: null, max: null };
|
|
if (path === "/api/temporal/snapshot") json = { active_node_ids: nodes.map((n) => n.id), active_node_count: nodes.length };
|
|
if (path === "/api/graph/search") json = { results: [{ node: nodes[0], score: 1 }] };
|
|
await route.fulfill({ json });
|
|
});
|
|
await page.goto(BASE_URL);
|
|
await page.getByRole("button", { name: "Open Semantica Explorer" }).click();
|
|
const legend = page.getByRole("group", { name: "Node colors" });
|
|
await legend.waitFor();
|
|
await page.locator("canvas").first().waitFor({ state: "visible" });
|
|
await assertLegendMatchesGraph(page);
|
|
assert.equal(await legend.getByText("Person", { exact: true }).count(), 1);
|
|
assert.equal(await legend.getByText("Biomolecule", { exact: true }).count(), 0);
|
|
|
|
nodes = initialNodes.map((node) => ({ ...node, type: node.type === "Person" ? "Researcher" : node.type }));
|
|
await page.getByRole("button", { name: "Reload graph data" }).click();
|
|
await legend.getByText("Researcher", { exact: true }).waitFor();
|
|
assert.equal(await legend.getByText("Person", { exact: true }).count(), 0);
|
|
await assertLegendMatchesGraph(page);
|
|
|
|
await page.getByPlaceholder("Search command, node, or concept").fill("Alice");
|
|
await page.getByRole("option").filter({ hasText: "Alice" }).click();
|
|
const heatmap = page.getByRole("button", { name: "Heatmap", exact: true });
|
|
await heatmap.click();
|
|
await legend.waitFor({ state: "hidden" });
|
|
await heatmap.click();
|
|
await legend.waitFor();
|
|
await assertLegendMatchesGraph(page);
|
|
const focusButton = page.getByRole("button", { name: "Focus", exact: true });
|
|
assert.equal(await focusButton.isDisabled(), false, "Focus is enabled once a node is selected");
|
|
await focusButton.click();
|
|
await legend.getByText("Document", { exact: true }).waitFor({ state: "hidden" });
|
|
await assertLegendMatchesGraph(page, ["alice", "acme", "research"]);
|
|
assert.equal(await legend.getByText("Researcher", { exact: true }).count(), 1);
|
|
await page.getByRole("button", { name: "Full Graph", exact: true }).click();
|
|
await legend.getByText("Document", { exact: true }).waitFor();
|
|
await assertLegendMatchesGraph(page);
|
|
assert.deepEqual(errors, []);
|
|
});
|