Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274import { describe, expect, it } from 'vitest';
import { clampRatioForAxis, createInitialTree, DEFAULT_SPLIT_RATIO, listTileIds, MIN_TILE_HEIGHT_PX, MIN_TILE_WIDTH_PX, planTileRemoval, removeLeaf, splitLeaf, updateSplitRatio, type SplitNode, type TileNode, type TilingNode,} from './model';
function tile(id: string): TileNode { return { kind: 'tile', id };}
function splitRow( id: string, ratio: number, first: TilingNode, second: TilingNode,): SplitNode { return { kind: 'split', id, orientation: 'row', ratio, first, second };}
describe('createInitialTree', () => { it('returns a single tile node', () => { expect(createInitialTree('a')).toEqual(tile('a')); });});
describe('splitLeaf', () => { it('wraps the active leaf in a split with the new tile as second child', () => { const tree = createInitialTree('left'); const out = splitLeaf(tree, 'left', 'column', { splitId: 'split-1', tileId: 'right', }); expect(out).not.toBeNull(); expect(out!.newTileId).toBe('right'); expect(out!.nextTree).toEqual({ kind: 'split', id: 'split-1', orientation: 'column', ratio: DEFAULT_SPLIT_RATIO, first: tile('left'), second: tile('right'), }); });
it('preserves sibling subtrees when splitting a nested leaf', () => { const tree: TilingNode = splitRow( 'root', 0.6, tile('keep'), tile('replace'), ); const out = splitLeaf(tree, 'replace', 'row', { splitId: 'nested', tileId: 'new', }); expect(out).not.toBeNull(); expect(out!.newTileId).toBe('new'); const next = out!.nextTree as SplitNode; expect(next.kind).toBe('split'); expect(next.first).toEqual(tile('keep')); expect(next.orientation).toBe('row'); expect(next.second).toMatchObject({ kind: 'split', id: 'nested', orientation: 'row', ratio: DEFAULT_SPLIT_RATIO, first: tile('replace'), second: tile('new'), }); });
it('places the new tile first when before=true', () => { const tree = createInitialTree('left'); const out = splitLeaf( tree, 'left', 'row', { splitId: 'split-1', tileId: 'right' }, true, ); expect(out!.nextTree).toEqual({ kind: 'split', id: 'split-1', orientation: 'row', ratio: DEFAULT_SPLIT_RATIO, first: tile('right'), second: tile('left'), }); });
it('searches depth-first along first branch before second', () => { const tree: TilingNode = splitRow('root', 0.5, tile('alpha'), tile('beta')); const out = splitLeaf(tree, 'beta', 'column', { splitId: 'inner', tileId: 'gamma', }); expect(out).not.toBeNull(); const root = out!.nextTree as SplitNode; expect(root.first).toEqual(tile('alpha')); const inner = root.second as SplitNode; expect(inner.id).toBe('inner'); expect(inner.second).toEqual(tile('gamma')); });
it('returns null when active tile does not exist', () => { const tree = createInitialTree('only'); expect( splitLeaf(tree, 'missing', 'row', { splitId: 's', tileId: 't' }), ).toBeNull(); });
it('does not mutate the original tree references', () => { const tree: TilingNode = splitRow('root', 0.25, tile('one'), tile('two')); const before = structuredClone(tree); splitLeaf(tree, 'two', 'column', { splitId: 's', tileId: 'three' }); expect(tree).toEqual(before); });});
describe('updateSplitRatio', () => { it('updates the matching split and leaves unrelated nodes unchanged', () => { const tree: TilingNode = splitRow( 's1', 0.3, tile('a'), splitRow('s2', 0.4, tile('b'), tile('c')), ); const next = updateSplitRatio(tree, 's2', 0.75); const root = next as SplitNode; expect(root.ratio).toBe(0.3); const inner = root.second as SplitNode; expect(inner.ratio).toBe(0.75); });
it('is a no-op for tile nodes', () => { expect(updateSplitRatio(tile('x'), 'any', 0.99)).toEqual(tile('x')); });});
describe('removeLeaf', () => { it('collapses a split into its sibling when removing the first child', () => { const tree = splitRow('root', 0.4, tile('a'), tile('b')); expect(removeLeaf(tree, 'a')).toEqual(tile('b')); });
it('collapses a split into its sibling when removing the second child', () => { const tree = splitRow('root', 0.4, tile('a'), tile('b')); expect(removeLeaf(tree, 'b')).toEqual(tile('a')); });
it('collapses a nested split, preserving the rest of the tree', () => { const tree: TilingNode = splitRow( 'root', 0.5, tile('keep'), splitRow('inner', 0.5, tile('gone'), tile('survivor')), ); expect(removeLeaf(tree, 'gone')).toEqual( splitRow('root', 0.5, tile('keep'), tile('survivor')), ); });
it('is a no-op when removing the only (root) tile', () => { const tree = createInitialTree('only'); expect(removeLeaf(tree, 'only')).toBe(tree); });
it('returns the same reference when the tile is not found', () => { const tree = splitRow('root', 0.5, tile('a'), tile('b')); expect(removeLeaf(tree, 'missing')).toBe(tree); });
it('does not mutate the original tree', () => { const tree: TilingNode = splitRow( 'root', 0.5, tile('keep'), splitRow('inner', 0.5, tile('gone'), tile('survivor')), ); const before = structuredClone(tree); removeLeaf(tree, 'gone'); expect(tree).toEqual(before); });});
describe('listTileIds', () => { it('returns the single id for a lone tile', () => { expect(listTileIds(tile('solo'))).toEqual(['solo']); });
it('lists tile ids left-to-right, depth-first', () => { const tree: TilingNode = splitRow( 'root', 0.5, tile('a'), splitRow('inner', 0.5, tile('b'), tile('c')), ); expect(listTileIds(tree)).toEqual(['a', 'b', 'c']); });});
describe('planTileRemoval', () => { it('reseeds when only one tile remains', () => { const tree = createInitialTree('only'); expect(planTileRemoval(tree, 'only', 'only')).toEqual({ kind: 'reseed' }); });
it('collapses the split and keeps focus when a non-active tile closes', () => { const tree = splitRow('root', 0.5, tile('a'), tile('b')); const plan = planTileRemoval(tree, 'b', 'a'); expect(plan).toEqual({ kind: 'collapse', nextTree: tile('a'), nextActiveTileId: 'a', }); });
it('moves focus to the first survivor when the active tile closes', () => { const tree: TilingNode = splitRow( 'root', 0.5, tile('a'), splitRow('inner', 0.5, tile('b'), tile('c')), ); const plan = planTileRemoval(tree, 'a', 'a'); expect(plan.kind).toBe('collapse'); if (plan.kind === 'collapse') { expect(listTileIds(plan.nextTree)).toEqual(['b', 'c']); expect(plan.nextActiveTileId).toBe('b'); } });});
describe('clampRatioForAxis', () => { it('returns the input ratio when axis size is non-positive', () => { expect(clampRatioForAxis(0.1, 'row', 0)).toBe(0.1); expect(clampRatioForAxis(0.9, 'column', -10)).toBe(0.9); });
it('respects MIN_TILE_WIDTH_PX for row orientation', () => { const w = 1000; const lo = MIN_TILE_WIDTH_PX / w; const hi = 1 - lo; expect(clampRatioForAxis(0, 'row', w)).toBeCloseTo(lo); expect(clampRatioForAxis(1, 'row', w)).toBeCloseTo(hi); expect(clampRatioForAxis(0.5, 'row', w)).toBe(0.5); });
it('respects MIN_TILE_HEIGHT_PX for column orientation', () => { const h = 800; const lo = MIN_TILE_HEIGHT_PX / h; const hi = 1 - lo; expect(clampRatioForAxis(0, 'column', h)).toBeCloseTo(lo); expect(clampRatioForAxis(1, 'column', h)).toBeCloseTo(hi); });
it('returns DEFAULT_SPLIT_RATIO when the axis is too small for two min panes', () => { const narrow = MIN_TILE_WIDTH_PX * 2 - 1; expect(clampRatioForAxis(0.2, 'row', narrow)).toBe(DEFAULT_SPLIT_RATIO); });});