Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions packages/memory-graph/src/__tests__/labels-layering.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* Tests for configurable labels and hover popover layering.
*
* Follows the same pattern as node-hover-popover.test.tsx: source-code
* assertions plus pure-logic tests on the exported defaults and merge
* behaviour, since hook-bearing components can't be mounted in this
* workspace's vitest environment.
*/

import { readFileSync } from "node:fs"
import { resolve } from "node:path"
import { describe, expect, it } from "vitest"
import { DEFAULT_HOVER_POPOVER_Z_INDEX, DEFAULT_LABELS } from "../constants"
import type { MemoryGraphLabels, ResolvedMemoryGraphLabels } from "../types"

const popoverSrc = readFileSync(
resolve(__dirname, "../components/node-hover-popover.tsx"),
"utf-8",
)
const legendSrc = readFileSync(
resolve(__dirname, "../components/legend.tsx"),
"utf-8",
)
const graphSrc = readFileSync(
resolve(__dirname, "../components/memory-graph.tsx"),
"utf-8",
)
const indexSrc = readFileSync(resolve(__dirname, "../index.tsx"), "utf-8")
const loadingSrc = readFileSync(
resolve(__dirname, "../components/loading-indicator.tsx"),
"utf-8",
)

describe("DEFAULT_LABELS — preserves original copy", () => {
it("matches every previously hardcoded string", () => {
expect(DEFAULT_LABELS.documentGroup).toBe("Documents")
expect(DEFAULT_LABELS.documentTypeFallback).toBe("document")
expect(DEFAULT_LABELS.documentSourceEdge).toBe("Document source")
expect(DEFAULT_LABELS.documentToMemoryEdge).toBe("Document to memory")
expect(DEFAULT_LABELS.documentIdLabel).toBe("Document")
expect(DEFAULT_LABELS.viewDocument).toBe("View document")
expect(DEFAULT_LABELS.goToDocument).toBe("Go to document")
expect(DEFAULT_LABELS.nextDocument).toBe("Next document")
expect(DEFAULT_LABELS.previousDocument).toBe("Prev document")
expect(DEFAULT_LABELS.showMemory).toBe("Go to memory")
})

it("memoryCount formats counts like the original template", () => {
expect(DEFAULT_LABELS.memoryCount(0)).toBe("0 memories")
expect(DEFAULT_LABELS.memoryCount(5)).toBe("5 memories")
})

it("loadingMoreDocuments formats counts like the original template", () => {
expect(DEFAULT_LABELS.loadingMoreDocuments(12)).toBe(
"Loading more documents... (12)",
)
})
})

describe("label merge — partial overrides win, defaults fill the rest", () => {
it("spread merge overrides only the provided keys", () => {
const overrides: MemoryGraphLabels = {
documentGroup: "Sources",
viewDocument: "View source",
}
const resolved: ResolvedMemoryGraphLabels = {
...DEFAULT_LABELS,
...overrides,
}
expect(resolved.documentGroup).toBe("Sources")
expect(resolved.viewDocument).toBe("View source")
expect(resolved.nextDocument).toBe("Next document")
expect(resolved.memoryCount(3)).toBe("3 memories")
})

it("custom memoryCount function replaces the default", () => {
const resolved: ResolvedMemoryGraphLabels = {
...DEFAULT_LABELS,
memoryCount: (count) => `${count} facts`,
}
expect(resolved.memoryCount(2)).toBe("2 facts")
})
})

describe("hover popover layering", () => {
it("default z-index stays at the previous hardcoded value", () => {
expect(DEFAULT_HOVER_POPOVER_Z_INDEX).toBe(100)
})

it("popover overlay uses the zIndex prop, not a literal", () => {
expect(popoverSrc).not.toContain("zIndex: 100")
const overlayIdx = popoverSrc.indexOf("const overlayStyle")
const overlayEnd = popoverSrc.indexOf("}", overlayIdx)
expect(popoverSrc.slice(overlayIdx, overlayEnd)).toContain("zIndex,")
})

it("popover defaults zIndex to DEFAULT_HOVER_POPOVER_Z_INDEX", () => {
expect(popoverSrc).toContain("zIndex = DEFAULT_HOVER_POPOVER_Z_INDEX")
})

it("MemoryGraph resolves layering.hoverPopoverZIndex with fallback", () => {
expect(graphSrc).toContain(
"layering?.hoverPopoverZIndex ?? DEFAULT_HOVER_POPOVER_Z_INDEX",
)
expect(graphSrc).toContain("zIndex={hoverPopoverZIndex}")
})
})

describe("no user-facing document strings remain hardcoded", () => {
it("popover uses labels for all document-facing copy", () => {
expect(popoverSrc).not.toContain('"View document"')
expect(popoverSrc).not.toContain('"Go to document"')
expect(popoverSrc).not.toContain('"Next document"')
expect(popoverSrc).not.toContain('"Prev document"')
expect(popoverSrc).not.toContain('label="Document"')
expect(popoverSrc).not.toContain('|| "document"')
expect(popoverSrc).toContain("labels.documentTypeFallback")
expect(popoverSrc).toContain("labels.memoryCount(")
expect(popoverSrc).toContain("labels.documentIdLabel")
expect(popoverSrc).toContain("labels.viewDocument")
expect(popoverSrc).toContain("labels.goToDocument")
expect(popoverSrc).toContain("labels.showMemory")
expect(popoverSrc).toContain("labels.nextDocument")
expect(popoverSrc).toContain("labels.previousDocument")
})

it("legend uses labels for group and edge copy", () => {
expect(legendSrc).not.toContain('label="Documents"')
expect(legendSrc).not.toContain("Document source</span>")
expect(legendSrc).not.toContain("Document to memory")
expect(legendSrc).toContain("labels.documentGroup")
expect(legendSrc).toContain("labels.documentSourceEdge")
expect(legendSrc).toContain("labels.documentToMemoryEdge")
})

it("loading indicator uses labels for the loading-more copy", () => {
expect(loadingSrc).not.toContain("Loading more documents")
expect(loadingSrc).toContain("labels.loadingMoreDocuments(totalLoaded)")
})

it("MemoryGraph merges label overrides and passes them down", () => {
expect(graphSrc).toContain("{ ...DEFAULT_LABELS, ...labelOverrides }")
expect(graphSrc).toContain("labels={resolvedLabels}")
})
})

describe("public API exports", () => {
it("index exports the new constants and types", () => {
expect(indexSrc).toContain("DEFAULT_LABELS")
expect(indexSrc).toContain("DEFAULT_HOVER_POPOVER_Z_INDEX")
expect(indexSrc).toContain("MemoryGraphLabels")
expect(indexSrc).toContain("MemoryGraphLayering")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ describe("'View document' button — render guard in JSX", () => {
expect(src).toContain("onClick={() => onOpenDocument(documentId)}")
})

it("button label text is 'View document'", () => {
expect(src).toContain('label="View document"')
it("button label comes from configurable labels", () => {
expect(src).toContain("label={labels.viewDocument}")
})

it("button icon is the EyeIcon component (not a string shortcut key)", () => {
Expand Down
101 changes: 101 additions & 0 deletions packages/memory-graph/src/__tests__/popover-render.e2e.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* Mounted-render verification for configurable labels and popover layering.
*/

// @vitest-environment happy-dom

import { cleanup, render } from "@testing-library/react"
import { afterEach, describe, expect, it } from "vitest"

afterEach(cleanup)
import { NodeHoverPopover } from "../components/node-hover-popover"
import { DEFAULT_COLORS, DEFAULT_LABELS } from "../constants"
import type { GraphNode, ResolvedMemoryGraphLabels } from "../types"

const documentNode: GraphNode = {
id: "doc-1",
type: "document",
x: 0,
y: 0,
data: {
id: "doc-1",
title: "Doc title",
summary: "Doc summary",
type: "",
createdAt: "2026-01-01",
updatedAt: "2026-01-01",
memories: [],
},
size: 40,
borderColor: "#fff",
isHovered: false,
isDragging: false,
}

function renderPopover(
labels?: ResolvedMemoryGraphLabels,
zIndex?: number,
onOpenDocument?: (id: string) => void,
) {
return render(
<NodeHoverPopover
colors={DEFAULT_COLORS}
labels={labels}
node={documentNode}
nodeRadius={20}
screenX={100}
screenY={100}
zIndex={zIndex}
onOpenDocument={onOpenDocument}
/>,
)
}

describe("NodeHoverPopover mounted render", () => {
it("renders default document copy when no labels are passed", () => {
const { container, getByText } = renderPopover(
undefined,
undefined,
() => {},
)
getByText("View document")
getByText("Go to memory")
getByText("Next document")
getByText("Prev document")
getByText("document")
getByText("Document")
getByText("0 memories")
const overlay = container.firstChild as HTMLElement
expect(overlay.style.zIndex).toBe("100")
})

it("renders custom source-facing copy and custom z-index", () => {
const labels: ResolvedMemoryGraphLabels = {
...DEFAULT_LABELS,
documentGroup: "Sources",
documentTypeFallback: "source",
documentIdLabel: "Source",
viewDocument: "View source",
goToDocument: "Go to source",
nextDocument: "Next source",
previousDocument: "Prev source",
showMemory: "Show memory",
memoryCount: (count) => `${count} facts`,
}
const { container, getByText, queryByText } = renderPopover(
labels,
30,
() => {},
)
getByText("View source")
getByText("Show memory")
getByText("Next source")
getByText("Prev source")
getByText("source")
getByText("Source")
getByText("0 facts")
expect(queryByText("View document")).toBeNull()
const overlay = container.firstChild as HTMLElement
expect(overlay.style.zIndex).toBe("30")
})
})
18 changes: 14 additions & 4 deletions packages/memory-graph/src/components/legend.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { memo, useState } from "react"
import type { GraphEdge, GraphNode, GraphThemeColors } from "../types"
import { DEFAULT_LABELS } from "../constants"
import type {
GraphEdge,
GraphNode,
GraphThemeColors,
ResolvedMemoryGraphLabels,
} from "../types"

interface LegendProps {
nodes?: GraphNode[]
edges?: GraphEdge[]
isLoading?: boolean
colors: GraphThemeColors
labels?: ResolvedMemoryGraphLabels
hoveredNode?: string | null
compact?: boolean
maxHeight?: number
Expand Down Expand Up @@ -291,6 +298,7 @@ export const Legend = memo(function Legend({
edges = [],
isLoading: _isLoading = false,
colors,
labels = DEFAULT_LABELS,
hoveredNode,
compact = false,
maxHeight,
Expand Down Expand Up @@ -462,7 +470,7 @@ export const Legend = memo(function Legend({
}}
/>
}
label="Documents"
label={labels.documentGroup}
colors={colors}
/>
<StatRow
Expand Down Expand Up @@ -523,12 +531,14 @@ export const Legend = memo(function Legend({
<div style={rowStyle}>
<div style={rowLeftStyle}>
<LineIcon color={colors.edgeDerives} />
<span style={edgeLabelStyle}>Document source</span>
<span style={edgeLabelStyle}>
{labels.documentSourceEdge}
</span>
</div>
<span style={countStyle}>{derivesCount}</span>
</div>
<div style={edgeDescriptionStyle}>
Document to memory
{labels.documentToMemoryEdge}
</div>
</div>
<div>
Expand Down
Loading