[refactor] Move pure functions from layout store to separate modules so they can be tested (and add tests) (#5462)

* refactor layout store utils

* [refactor] use nullish coalescing in getOr helper - addresses @DrJKL's suggestion

Replaces manual undefined/null checks with more concise ?? operator
for cleaner code that achieves the same functionality.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [refactor] improve Y.Map typing for better type safety - addresses @DrJKL's typing suggestions

- Use Y.Map<NodeLayout[keyof NodeLayout]> instead of Y.Map<unknown>
- Provides compile-time type safety for stored values
- Improves IntelliSense and prevents type mismatches
- Updates mappers, store, tests, and helper functions consistently
- No runtime changes, pure TypeScript improvement

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [refactor] address @arjansingh code quality feedback

- Remove AI-generated refactoring comment that adds no value
- Reorganize tests with nested describe blocks for better readability
- Group related test cases by function for easier scanning

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [refactor] move makeLinkSegmentKey to layoutUtils - addresses @arjansingh's file organization feedback

- Move string concatenation function from layoutMath.ts to new layoutUtils.ts
- Keep layoutMath.ts focused on pure geometric calculations
- Create dedicated layoutUtils.ts for general layout utilities
- Update imports in store and create separate test file
- Improves module cohesion and clarity of purpose

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [cleanup] remove leftover AI refactoring comments

- Remove "Constants moved to utils" and "Node layout mapping moved to utils"
- Clean up extra blank lines from previous refactoring
- Keep meaningful organizational comments like "Helper methods"

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [cleanup] remove unnecessary import aliases

Remove pointInBoundsUtil/boundsIntersectUtil aliases as there are no
naming conflicts. Use direct function names for cleaner code.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [refactor] improve Y.Map typing with named NodeLayoutMap type - addresses @DrJKL's performance and type safety suggestions

- Create named NodeLayoutMap type for TypeScript performance optimization
- Improve getOr function with proper key constraints and type safety
- Update all Y.Map<NodeLayout[keyof NodeLayout]> usages to use NodeLayoutMap
- Remove manual type assertions in favor of generic key constraints
- Clean up unused imports and fix formatting issues

* [cleanup] remove explanatory comment per @DrJKL's preference

* don't wait for dialog close button to be stable

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-09-13 21:17:09 -07:00
committed by GitHub
parent 6f878abea4
commit 90bf8dc74a
8 changed files with 222 additions and 101 deletions

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest'
import {
REROUTE_RADIUS,
boundsIntersect,
pointInBounds
} from '@/renderer/core/layout/utils/layoutMath'
describe('layoutMath utils', () => {
describe('pointInBounds', () => {
it('detects inclusion correctly', () => {
const bounds = { x: 10, y: 10, width: 100, height: 50 }
expect(pointInBounds({ x: 10, y: 10 }, bounds)).toBe(true)
expect(pointInBounds({ x: 110, y: 60 }, bounds)).toBe(true)
expect(pointInBounds({ x: 9, y: 10 }, bounds)).toBe(false)
expect(pointInBounds({ x: 111, y: 10 }, bounds)).toBe(false)
expect(pointInBounds({ x: 10, y: 61 }, bounds)).toBe(false)
})
it('works with zero-size bounds', () => {
const zero = { x: 10, y: 20, width: 0, height: 0 }
expect(pointInBounds({ x: 10, y: 20 }, zero)).toBe(true)
expect(pointInBounds({ x: 10, y: 21 }, zero)).toBe(false)
expect(pointInBounds({ x: 9, y: 20 }, zero)).toBe(false)
})
})
describe('boundsIntersect', () => {
it('detects intersection correctly', () => {
const a = { x: 0, y: 0, width: 10, height: 10 }
const b = { x: 5, y: 5, width: 10, height: 10 }
const c = { x: 11, y: 0, width: 5, height: 5 }
expect(boundsIntersect(a, b)).toBe(true)
expect(boundsIntersect(a, c)).toBe(false)
})
it('treats touching edges as intersecting', () => {
const a = { x: 0, y: 0, width: 10, height: 10 }
const d = { x: 10, y: 0, width: 5, height: 5 } // touches at right edge
expect(boundsIntersect(a, d)).toBe(true)
})
})
describe('REROUTE_RADIUS', () => {
it('exports a sensible reroute radius', () => {
expect(REROUTE_RADIUS).toBeGreaterThan(0)
})
})
})

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { makeLinkSegmentKey } from '@/renderer/core/layout/utils/layoutUtils'
describe('layoutUtils', () => {
describe('makeLinkSegmentKey', () => {
it('creates stable keys for null reroute', () => {
expect(makeLinkSegmentKey(10, null)).toBe('10:final')
expect(makeLinkSegmentKey(42, null)).toBe('42:final')
})
it('creates stable keys for numeric reroute ids', () => {
expect(makeLinkSegmentKey(10, 3)).toBe('10:3')
expect(makeLinkSegmentKey(42, 0)).toBe('42:0')
expect(makeLinkSegmentKey(42, 7)).toBe('42:7')
})
})
})

View File

@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import * as Y from 'yjs'
import {
NODE_LAYOUT_DEFAULTS,
type NodeLayoutMap,
yNodeToLayout
} from '@/renderer/core/layout/utils/mappers'
describe('mappers', () => {
it('yNodeToLayout reads from Yjs-attached map', () => {
const layout = {
id: 'node-1',
position: { x: 12, y: 34 },
size: { width: 111, height: 222 },
zIndex: 5,
visible: true,
bounds: { x: 12, y: 34, width: 111, height: 222 }
}
const doc = new Y.Doc()
const ynode = doc.getMap('node') as NodeLayoutMap
ynode.set('id', layout.id)
ynode.set('position', layout.position)
ynode.set('size', layout.size)
ynode.set('zIndex', layout.zIndex)
ynode.set('visible', layout.visible)
ynode.set('bounds', layout.bounds)
const back = yNodeToLayout(ynode)
expect(back).toEqual(layout)
})
it('yNodeToLayout applies defaults for missing fields', () => {
const doc = new Y.Doc()
const ynode = doc.getMap('node') as NodeLayoutMap
// Don't set any fields - they should all use defaults
const back = yNodeToLayout(ynode)
expect(back.id).toBe(NODE_LAYOUT_DEFAULTS.id)
expect(back.position).toEqual(NODE_LAYOUT_DEFAULTS.position)
expect(back.size).toEqual(NODE_LAYOUT_DEFAULTS.size)
expect(back.zIndex).toEqual(NODE_LAYOUT_DEFAULTS.zIndex)
expect(back.visible).toEqual(NODE_LAYOUT_DEFAULTS.visible)
expect(back.bounds).toEqual(NODE_LAYOUT_DEFAULTS.bounds)
})
})