|
| 1 | +import { describe, it, expect, vi } from 'vitest' |
| 2 | +import { firstSelectedIndex } from './first-selected-index' |
| 3 | +import { createSelectionState } from './selection-state.svelte' |
| 4 | + |
| 5 | +describe('firstSelectedIndex', () => { |
| 6 | + it('returns the lowest index when there is no parent row', () => { |
| 7 | + expect(firstSelectedIndex([2, 5, 7], false)).toBe(2) |
| 8 | + }) |
| 9 | + |
| 10 | + it('never lands on the `..` row: skips a leading index 0 when hasParent', () => { |
| 11 | + // A `*`-style match can include the synthetic `..` at 0; the cursor must skip it. |
| 12 | + expect(firstSelectedIndex([0, 3, 4], true)).toBe(3) |
| 13 | + }) |
| 14 | + |
| 15 | + it('keeps index 0 when there is NO parent row (0 is a real file)', () => { |
| 16 | + expect(firstSelectedIndex([0, 3, 4], false)).toBe(0) |
| 17 | + }) |
| 18 | + |
| 19 | + it('returns the only selectable index past the parent row', () => { |
| 20 | + expect(firstSelectedIndex([0, 9], true)).toBe(9) |
| 21 | + }) |
| 22 | + |
| 23 | + it('returns null for an empty set', () => { |
| 24 | + expect(firstSelectedIndex([], true)).toBeNull() |
| 25 | + expect(firstSelectedIndex([], false)).toBeNull() |
| 26 | + }) |
| 27 | + |
| 28 | + it('returns null when the only entry is the `..` row under hasParent', () => { |
| 29 | + expect(firstSelectedIndex([0], true)).toBeNull() |
| 30 | + }) |
| 31 | +}) |
| 32 | + |
| 33 | +/** |
| 34 | + * Pins `FilePane.applyIndices`'s post-select cursor jump without mounting the component |
| 35 | + * (same approach as `has-parent.test.ts` pairing the pure helper with real `selectAll`). |
| 36 | + * Replicates the method body: apply to the real selection state, then move the cursor on |
| 37 | + * `add` only. `setCursorIndex` is the pane's cursor-move + scroll-into-view primitive, so a |
| 38 | + * call on it IS the scroll-into-view request. |
| 39 | + */ |
| 40 | +function runApplyIndices( |
| 41 | + idxs: number[], |
| 42 | + mode: 'add' | 'remove', |
| 43 | + hasParent: boolean, |
| 44 | + setCursorIndex: (index: number) => void, |
| 45 | +): void { |
| 46 | + const selection = createSelectionState() |
| 47 | + selection.applyIndices(idxs, mode, hasParent) |
| 48 | + if (mode === 'add') { |
| 49 | + const target = firstSelectedIndex(idxs, hasParent) |
| 50 | + if (target !== null) setCursorIndex(target) |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +describe('applyIndices post-select cursor jump (integration)', () => { |
| 55 | + it('moves the cursor to the first selected file on add, scrolling it into view', () => { |
| 56 | + const setCursorIndex = vi.fn() |
| 57 | + runApplyIndices([2, 5, 7], 'add', false, setCursorIndex) |
| 58 | + expect(setCursorIndex).toHaveBeenCalledTimes(1) |
| 59 | + expect(setCursorIndex).toHaveBeenCalledWith(2) |
| 60 | + }) |
| 61 | + |
| 62 | + it('lands on the first real file, never the `..` row, when hasParent', () => { |
| 63 | + const setCursorIndex = vi.fn() |
| 64 | + // The match included the synthetic `..` at index 0; the cursor must skip it. |
| 65 | + runApplyIndices([0, 4, 8], 'add', true, setCursorIndex) |
| 66 | + expect(setCursorIndex).toHaveBeenCalledWith(4) |
| 67 | + }) |
| 68 | + |
| 69 | + it('does NOT move the cursor on remove (deselect)', () => { |
| 70 | + const setCursorIndex = vi.fn() |
| 71 | + runApplyIndices([2, 5, 7], 'remove', false, setCursorIndex) |
| 72 | + expect(setCursorIndex).not.toHaveBeenCalled() |
| 73 | + }) |
| 74 | + |
| 75 | + it('does not move the cursor when an add selects nothing selectable', () => { |
| 76 | + const setCursorIndex = vi.fn() |
| 77 | + runApplyIndices([0], 'add', true, setCursorIndex) // only `..`, which is skipped |
| 78 | + expect(setCursorIndex).not.toHaveBeenCalled() |
| 79 | + }) |
| 80 | +}) |
0 commit comments