mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
Move source control selection refs out of effects
This commit is contained in:
+88
@@ -0,0 +1,88 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, Suspense, startTransition, useRef, useState } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GitStatusEntry } from '../../../../shared/types'
|
||||
import { useSourceControlSelection, type FlatEntry } from './useSourceControlSelection'
|
||||
|
||||
const OLD_ENTRY: FlatEntry = {
|
||||
key: 'unstaged::old.ts',
|
||||
area: 'unstaged',
|
||||
entry: { path: 'old.ts', area: 'unstaged', status: 'modified' }
|
||||
}
|
||||
|
||||
const NEW_ENTRY: FlatEntry = {
|
||||
key: 'unstaged::new.ts',
|
||||
area: 'unstaged',
|
||||
entry: { path: 'new.ts', area: 'unstaged', status: 'modified' }
|
||||
}
|
||||
|
||||
describe('useSourceControlSelection committed event state', () => {
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
container.remove()
|
||||
})
|
||||
|
||||
it('keeps the visible row callback while the next render is suspended', async () => {
|
||||
const openedOld = vi.fn<(entry: GitStatusEntry) => void>()
|
||||
const openedNew = vi.fn<(entry: GitStatusEntry) => void>()
|
||||
const neverResolves = new Promise<never>(() => {})
|
||||
let showNext!: () => void
|
||||
|
||||
function SelectionHarness({ next }: { next: boolean }): React.JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const selection = useSourceControlSelection({
|
||||
flatEntries: next ? [NEW_ENTRY] : [OLD_ENTRY],
|
||||
onOpenDiff: next ? openedNew : openedOld,
|
||||
containerRef
|
||||
})
|
||||
|
||||
if (next) {
|
||||
throw neverResolves
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<button
|
||||
onClick={(event) => selection.handleSelect(event, OLD_ENTRY.key, OLD_ENTRY.entry)}
|
||||
>
|
||||
Open old row
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
const [next, setNext] = useState(false)
|
||||
showNext = () => startTransition(() => setNext(true))
|
||||
return (
|
||||
<Suspense fallback={<div>Loading next worktree</div>}>
|
||||
<SelectionHarness next={next} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
await act(async () => root.render(<App />))
|
||||
await act(async () => {
|
||||
showNext()
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
const visibleButton = container.querySelector('button')
|
||||
expect(visibleButton?.textContent).toBe('Open old row')
|
||||
visibleButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
|
||||
expect(openedOld).toHaveBeenCalledWith(OLD_ENTRY.entry)
|
||||
expect(openedNew).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useEffect, useRef, type RefObject } from 'react'
|
||||
import { useState, useCallback, useEffect, useLayoutEffect, useRef, type RefObject } from 'react'
|
||||
import type { GitStatusEntry } from '../../../../shared/types'
|
||||
import type { SourceControlRowOpenEvent } from './source-control-split-open'
|
||||
|
||||
@@ -91,25 +91,15 @@ export function useSourceControlSelection({
|
||||
const onOpenDiffRef = useRef(onOpenDiff)
|
||||
const shouldOpenAsSplitRef = useRef(shouldOpenAsSplit)
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
// Why: stable row handlers must read one committed selection/worktree
|
||||
// snapshot; render-time writes can leak values from an interrupted render.
|
||||
flatEntriesRef.current = flatEntries
|
||||
}, [flatEntries])
|
||||
|
||||
useEffect(() => {
|
||||
anchorKeyRef.current = anchorKey
|
||||
}, [anchorKey])
|
||||
|
||||
useEffect(() => {
|
||||
selectedKeysRef.current = selectedKeys
|
||||
}, [selectedKeys])
|
||||
|
||||
useEffect(() => {
|
||||
onOpenDiffRef.current = onOpenDiff
|
||||
}, [onOpenDiff])
|
||||
|
||||
useEffect(() => {
|
||||
shouldOpenAsSplitRef.current = shouldOpenAsSplit
|
||||
}, [shouldOpenAsSplit])
|
||||
}, [flatEntries, anchorKey, selectedKeys, onOpenDiff, shouldOpenAsSplit])
|
||||
|
||||
const reconciledSelection = reconcileSourceControlSelectionState({
|
||||
selectedKeys,
|
||||
|
||||
Reference in New Issue
Block a user