From a55dca4ecb5bf72c4b320a1df40bac195eb8969a Mon Sep 17 00:00:00 2001
From: d-oit <6849456+d-oit@users.noreply.github.com>
Date: Thu, 30 Jul 2026 20:57:02 +0200
Subject: [PATCH] =?UTF-8?q?test:=20add=20branch-coverage=20tests=20for=20g?=
=?UTF-8?q?raph-view.tsx=20(59.84%=20=E2=86=92=20~70%=20branches)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../studio/views/graph-view-coverage.test.tsx | 314 ++++++++++++++++++
1 file changed, 314 insertions(+)
create mode 100644 src/components/studio/views/graph-view-coverage.test.tsx
diff --git a/src/components/studio/views/graph-view-coverage.test.tsx b/src/components/studio/views/graph-view-coverage.test.tsx
new file mode 100644
index 00000000..a4c729be
--- /dev/null
+++ b/src/components/studio/views/graph-view-coverage.test.tsx
@@ -0,0 +1,314 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import type { ReactNode } from 'react'
+import { render, screen, fireEvent, cleanup } from '@testing-library/react'
+
+vi.mock('lucide-react', () => {
+ const I = ({ className }: { className?: string }) => (
+
+ )
+ return {
+ CircleDot: I,
+ Circle: I,
+ GitFork: I,
+ Layers: I,
+ Focus: I,
+ Camera: I,
+ RotateCcw: I,
+ RotateCw: I,
+ Download: I,
+ }
+})
+
+vi.mock('@/lib/studio/use-reduced-motion', () => ({
+ useReducedMotion: () => true,
+}))
+
+vi.mock('@/lib/utils', () => ({
+ cn: (...args: (string | undefined | false | null)[]) => args.filter(Boolean).join(' '),
+}))
+
+vi.mock('../ui/shared-primitives', () => ({
+ ToggleButtonGroup: ({ children, label }: { children?: ReactNode; label?: string }) => (
+
{children}
+ ),
+ Divider: () =>
,
+}))
+
+vi.mock('sonner', () => ({
+ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() },
+}))
+
+const mockSelectEntity = vi.fn()
+const mockUndo = vi.fn()
+const mockRedo = vi.fn()
+
+const mockEntities = [
+ {
+ id: 'ent-1',
+ name: 'Test Entity',
+ type: 'note' as const,
+ description: 'desc',
+ content: '',
+ tags: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ links: [{ targetId: 'ent-2', relation: 'related' }],
+ },
+ {
+ id: 'ent-2',
+ name: 'Linked Entity',
+ type: 'concept' as const,
+ description: 'desc',
+ content: '',
+ tags: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ links: [],
+ },
+]
+
+let currentEntities = mockEntities
+let currentSelectedEntityId: string | null = null
+let currentHistoryIndex = 0
+let currentEntityHistory: unknown[][] = [[]]
+
+vi.mock('@/lib/studio/store', () => ({
+ useStudioStore: (selector: (s: Record) => unknown) =>
+ selector({
+ entities: currentEntities,
+ selectedEntityId: currentSelectedEntityId,
+ selectEntity: mockSelectEntity,
+ undo: mockUndo,
+ redo: mockRedo,
+ entityHistory: currentEntityHistory,
+ historyIndex: currentHistoryIndex,
+ }),
+}))
+
+import { GraphView } from './graph-view'
+
+describe('GraphView branch coverage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ currentEntities = mockEntities
+ currentSelectedEntityId = null
+ currentHistoryIndex = 0
+ currentEntityHistory = [[]]
+ vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {})
+ vi.spyOn(Storage.prototype, 'getItem').mockReturnValue(null)
+ })
+
+ afterEach(() => {
+ cleanup()
+ vi.restoreAllMocks()
+ })
+
+ it('renders empty state when no entities', () => {
+ currentEntities = []
+ render()
+ expect(screen.getByText('No entities to graph yet.')).toBeDefined()
+ })
+
+ it('switches to circular layout when button clicked', () => {
+ render()
+ const circularBtn = screen.getByText('circular')
+ fireEvent.click(circularBtn)
+ expect(circularBtn).toHaveAttribute('aria-pressed', 'true')
+ })
+
+ it('switches to hierarchical layout when button clicked', () => {
+ render()
+ const hierBtn = screen.getByText('hierarchical')
+ fireEvent.click(hierBtn)
+ expect(hierBtn).toHaveAttribute('aria-pressed', 'true')
+ })
+
+ it('switches back to force layout when force button clicked', () => {
+ render()
+ fireEvent.click(screen.getByText('circular'))
+ fireEvent.click(screen.getByText('force'))
+ expect(screen.getByText('force')).toHaveAttribute('aria-pressed', 'true')
+ })
+
+ it('toggles focus mode on and off', () => {
+ currentSelectedEntityId = 'ent-1'
+ render()
+ const focusBtn = screen.getByLabelText('Focus neighborhood')
+ fireEvent.click(focusBtn)
+ expect(focusBtn).toHaveAttribute('aria-pressed', 'true')
+ fireEvent.click(focusBtn)
+ expect(focusBtn).toHaveAttribute('aria-pressed', 'false')
+ })
+
+ it('selects entity on node click', () => {
+ render()
+ const node = screen.getByRole('button', { name: /Test Entity/ })
+ fireEvent.click(node)
+ expect(mockSelectEntity).toHaveBeenCalledWith('ent-1')
+ })
+
+ it('deselects entity on click when already selected', () => {
+ currentSelectedEntityId = 'ent-1'
+ render()
+ const node = screen.getByRole('button', { name: /Test Entity/ })
+ fireEvent.click(node)
+ expect(mockSelectEntity).toHaveBeenCalledWith(null)
+ })
+
+ it('selects entity on Enter key', () => {
+ render()
+ const node = screen.getByRole('button', { name: /Test Entity/ })
+ fireEvent.keyDown(node, { key: 'Enter' })
+ expect(mockSelectEntity).toHaveBeenCalledWith('ent-1')
+ })
+
+ it('selects entity on Space key', () => {
+ render()
+ const node = screen.getByRole('button', { name: /Test Entity/ })
+ fireEvent.keyDown(node, { key: ' ' })
+ expect(mockSelectEntity).toHaveBeenCalledWith('ent-1')
+ })
+
+ it('focuses node and tracks focusedNodeId', () => {
+ render()
+ const node = screen.getByRole('button', { name: /Test Entity/ })
+ fireEvent.focus(node)
+ // Focus sets focusedNodeId to the node id
+ expect(node).toHaveAttribute('tabIndex', '0')
+ })
+
+ it('calls undo when undo button clicked', () => {
+ currentHistoryIndex = 1
+ currentEntityHistory = [[], []]
+ render()
+ const undoBtn = screen.getByLabelText('Undo')
+ fireEvent.click(undoBtn)
+ expect(mockUndo).toHaveBeenCalled()
+ })
+
+ it('calls redo when redo button clicked', () => {
+ currentEntityHistory = [[], []]
+ render()
+ const redoBtn = screen.getByLabelText('Redo')
+ fireEvent.click(redoBtn)
+ expect(mockRedo).toHaveBeenCalled()
+ })
+
+ it('saves snapshot when save snapshot button clicked', () => {
+ render()
+ const snapshotBtn = screen.getByLabelText('Save snapshot')
+ fireEvent.click(snapshotBtn)
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ 'dks-graph-snapshot',
+ expect.any(String),
+ )
+ })
+
+ it('renders entity type legend', () => {
+ render()
+ expect(screen.getByText('Entity types')).toBeDefined()
+ expect(screen.getByText('Note')).toBeDefined()
+ expect(screen.getByText('Concept')).toBeDefined()
+ expect(screen.getByText('Person')).toBeDefined()
+ expect(screen.getByText('Project')).toBeDefined()
+ })
+
+ it('shows correct node and edge counts', () => {
+ render()
+ expect(screen.getByText(/2 nodes · 1 edges/)).toBeDefined()
+ })
+
+ it('shows correct counts with focus mode enabled (only selected + neighbors)', () => {
+ currentSelectedEntityId = 'ent-1'
+ render()
+ const focusBtn = screen.getByLabelText('Focus neighborhood')
+ fireEvent.click(focusBtn)
+ expect(screen.getByText(/2 nodes · 1 edges/)).toBeDefined()
+ })
+
+ it('removes edges when entity type is filtered', () => {
+ // When a node has a link to a non-existent target, the link should be filtered out
+ const entitiesWithBadLink = [
+ {
+ id: 'ent-1',
+ name: 'Entity 1',
+ type: 'note' as const,
+ description: '',
+ content: '',
+ tags: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ links: [{ targetId: 'non-existent', relation: 'related' }],
+ },
+ ]
+ currentEntities = entitiesWithBadLink
+ render()
+ expect(screen.getByText(/1 nodes · 0 edges/)).toBeDefined()
+ })
+
+ it('removes duplicate edges between same nodes', () => {
+ const entitiesWithDupLinks = [
+ {
+ id: 'ent-1',
+ name: 'Entity 1',
+ type: 'note' as const,
+ description: '',
+ content: '',
+ tags: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ links: [
+ { targetId: 'ent-2', relation: 'related' },
+ { targetId: 'ent-2', relation: 'related' },
+ ],
+ },
+ {
+ id: 'ent-2',
+ name: 'Entity 2',
+ type: 'note' as const,
+ description: '',
+ content: '',
+ tags: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ links: [],
+ },
+ ]
+ currentEntities = entitiesWithDupLinks
+ render()
+ expect(screen.getByText(/2 nodes · 1 edges/)).toBeDefined()
+ })
+
+ it('renders edge label when a node is selected (highlighted edge)', () => {
+ currentSelectedEntityId = 'ent-1'
+ render()
+ // The relation label is rendered as SVG in the highlighted edge
+ expect(screen.getByRole('img', { name: /knowledge graph/ })).toBeDefined()
+ })
+
+ it('shows label truncation for long names', () => {
+ const longNameEntity = [
+ {
+ id: 'ent-1',
+ name: 'A very long entity name that should be truncated at 24 characters',
+ type: 'note' as const,
+ description: '',
+ content: '',
+ tags: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ links: [],
+ },
+ ]
+ currentEntities = longNameEntity
+ render()
+ const node = screen.getByRole('button', { name: /A very long entity/ })
+ expect(node).toBeDefined()
+ })
+
+ it('renders no edges for empty entity list', () => {
+ currentEntities = []
+ render()
+ expect(screen.getByText('No entities to graph yet.')).toBeDefined()
+ })
+})