diff --git a/src/components/__tests__/LayoutTree.ts b/src/components/__tests__/LayoutTree.ts index 80656ce0c15..a7580add0e7 100644 --- a/src/components/__tests__/LayoutTree.ts +++ b/src/components/__tests__/LayoutTree.ts @@ -2,10 +2,14 @@ import { act } from 'react' import { importTextActionCreator as importText } from '../../actions/importText' import createTestApp, { cleanupTestApp } from '../../test-helpers/createTestApp' import dispatch from '../../test-helpers/dispatch' +import queryThoughtByText from '../../test-helpers/queries/queryThoughtByText' import { setCursorFirstMatchActionCreator as setCursor } from '../../test-helpers/setCursorFirstMatch' beforeEach(createTestApp) -afterEach(cleanupTestApp) +afterEach(async () => { + vi.restoreAllMocks() + await cleanupTestApp() +}) it('unmount TreeNodes on collapse', async () => { await dispatch( @@ -34,3 +38,35 @@ it('unmount TreeNodes on collapse', async () => { // collapse a and unmount b expect(document.querySelectorAll('[aria-label="tree-node"]').length).toBe(2) }) + +it('unmounts distant thoughts after navigation and remounts them on return', async () => { + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 0, 100, 20)) + + await dispatch( + importText({ + text: ` + - a + - b + - c + - d + - e + - f + - g + `, + }), + ) + + await dispatch(setCursor(['a'])) + expect(await queryThoughtByText('a')).not.toBeNull() + + await dispatch(setCursor(['a', 'b', 'c', 'd', 'e', 'f'])) + await act(vi.runOnlyPendingTimersAsync) + + expect(await queryThoughtByText('a')).toBeNull() + // hide-parent thoughts stay mounted so they can fade back in when navigating up. + expect(await queryThoughtByText('d')).not.toBeNull() + expect(await queryThoughtByText('f')).not.toBeNull() + + await dispatch(setCursor(['a'])) + expect(await queryThoughtByText('a')).not.toBeNull() +}) diff --git a/src/e2e/puppeteer/__tests__/cursor.ts b/src/e2e/puppeteer/__tests__/cursor.ts index 40139a5c0ca..69fd2e34bdf 100644 --- a/src/e2e/puppeteer/__tests__/cursor.ts +++ b/src/e2e/puppeteer/__tests__/cursor.ts @@ -10,6 +10,19 @@ import waitUntil from '../helpers/waitUntil' vi.setConfig({ testTimeout: 20000, hookTimeout: 20000 }) +/** Returns the persistent tree node that contains the editable with the given value. */ +const getTreeNode = async (value: string) => { + const editable = (await waitForEditable(value)).asElement() + if (!editable) throw new Error(`Editable "${value}" not found.`) + + const treeNode = ( + await editable.evaluateHandle(element => (element as Element).closest('[aria-label="tree-node"]')) + ).asElement() + if (!treeNode) throw new Error(`Tree node for "${value}" not found.`) + + return treeNode +} + it('set the cursor to a thought in the home context on load', async () => { const importText = ` - a @@ -92,8 +105,10 @@ it('do nothing when clicking on a hidden ancestor', async () => { - d` await paste(importText) await waitForEditable('d') + const ancestorTreeNode = await getTreeNode('a') await clickThought('d') - await clickThought('a') + await ancestorTreeNode.waitForSelector('[data-editable]', { hidden: true }) + await click(ancestorTreeNode) const thoughtValue = await getEditingText() expect(thoughtValue).toBe('d') @@ -107,6 +122,8 @@ it('do nothing when clicking on a hidden great uncle', async () => { - d` await paste(importText) + const greatUncleTreeNode = await getTreeNode('d') + // click a to expand b and c await waitForEditable('a') await clickThought('a') @@ -115,7 +132,8 @@ it('do nothing when clicking on a hidden great uncle', async () => { // for some reason we need to sleep before clicking c, otherwise the cursor is moved to d await waitForEditable('c') await clickThought('c') - await clickThought('d') + await greatUncleTreeNode.waitForSelector('[data-editable]', { hidden: true }) + await click(greatUncleTreeNode) const thoughtValue = await getEditingText() expect(thoughtValue).toBe('c') diff --git a/src/hooks/__tests__/useDelayedAutofocus.ts b/src/hooks/__tests__/useDelayedAutofocus.ts new file mode 100644 index 00000000000..f27b961d4ba --- /dev/null +++ b/src/hooks/__tests__/useDelayedAutofocus.ts @@ -0,0 +1,45 @@ +import { renderHook } from '@testing-library/react' +import { act } from 'react' +import Autofocus from '../../@types/Autofocus' +import useDelayedAutofocus from '../useDelayedAutofocus' + +beforeEach(() => { + vi.useFakeTimers() +}) + +afterEach(() => { + vi.useRealTimers() +}) + +it('updates the selected autofocus after the delay', () => { + const { result, rerender } = renderHook( + ({ autofocus }: { autofocus: Autofocus }) => + useDelayedAutofocus(autofocus, { + delay: 750, + selector: autofocusNew => autofocusNew === 'hide', + }), + { initialProps: { autofocus: 'show' as Autofocus } }, + ) + + rerender({ autofocus: 'hide' }) + + act(() => vi.advanceTimersByTime(749)) + expect(result.current).toBe(false) + + act(() => vi.advanceTimersByTime(1)) + expect(result.current).toBe(true) +}) + +it('cancels a delayed update on unmount', () => { + const { rerender, unmount } = renderHook( + ({ autofocus }: { autofocus: Autofocus }) => + useDelayedAutofocus(autofocus, { delay: 750, selector: value => value }), + { initialProps: { autofocus: 'show' as Autofocus } }, + ) + + rerender({ autofocus: 'hide' }) + expect(vi.getTimerCount()).toBe(1) + + unmount() + expect(vi.getTimerCount()).toBe(0) +}) diff --git a/src/hooks/useDelayedAutofocus.ts b/src/hooks/useDelayedAutofocus.ts index e780c22cae5..cbfabdbc10b 100644 --- a/src/hooks/useDelayedAutofocus.ts +++ b/src/hooks/useDelayedAutofocus.ts @@ -14,7 +14,6 @@ const useDelayedAutofocus = ( // This ensures that the component is only re-rendered when the selector result changes, not every time the delayed autofocus value changes. const [autofocusDelayed, setAutofocusDelayed] = useState(selector(autofocus)) const lastAutofocusRef = useRef(autofocus) - const unmounted = useRef(false) const autofocusTimerRef = useRef(0) useEffect( () => { @@ -25,7 +24,6 @@ const useDelayedAutofocus = ( (lastAutofocusRef.current === 'show' || lastAutofocusRef.current === 'dim') ) { autofocusTimerRef.current = setTimeout(() => { - if (unmounted.current) return setAutofocusDelayed(selector(autofocus)) lastAutofocusRef.current = autofocus }, delay) as unknown as number @@ -35,7 +33,7 @@ const useDelayedAutofocus = ( } return () => { - unmounted.current = true + clearTimeout(autofocusTimerRef.current) } }, // eslint-disable-next-line react-hooks/exhaustive-deps