diff --git a/src/components/CitationExporter/useCitationExporter.test.tsx b/src/components/CitationExporter/useCitationExporter.test.tsx new file mode 100644 index 000000000..b4ebb5834 --- /dev/null +++ b/src/components/CitationExporter/useCitationExporter.test.tsx @@ -0,0 +1,49 @@ +import { act } from '@testing-library/react'; +import { describe, expect, test, vi } from 'vitest'; + +import { ExportApiFormatKey } from '@/api/export/types'; +import { renderHook } from '@/test-utils'; + +import { useCitationExporter } from './useCitationExporter'; + +vi.mock('next/router', () => ({ + useRouter: () => ({ pathname: '/', push: vi.fn(), asPath: '/', query: {}, beforePopState: vi.fn() }), +})); + +const baseProps = { + records: ['2021APS..APRA01003G'], + format: ExportApiFormatKey.bibtex, + singleMode: false, +}; + +describe('useCitationExporter — prop ↔ machine sync', () => { + test('SET_FORMAT dispatched from UI persists and is not reverted by the prop-sync effect', () => { + const { result, rerender } = renderHook(() => useCitationExporter(baseProps)); + + expect(result.current.state.context.params.format).toBe(ExportApiFormatKey.bibtex); + + act(() => { + result.current.dispatch({ type: 'SET_FORMAT', payload: ExportApiFormatKey.endnote }); + }); + + expect(result.current.state.context.params.format).toBe(ExportApiFormatKey.endnote); + + // Re-render with the same (unchanged) props. The prop-sync effect must not + // revert the user's choice back to the original `format` prop. + rerender(); + expect(result.current.state.context.params.format).toBe(ExportApiFormatKey.endnote); + }); + + test('prop change still syncs into the machine', () => { + const { result, rerender } = renderHook( + ({ format }: { format: ExportApiFormatKey }) => useCitationExporter({ ...baseProps, format }), + undefined, + { initialProps: { format: ExportApiFormatKey.bibtex } }, + ); + + expect(result.current.state.context.params.format).toBe(ExportApiFormatKey.bibtex); + + rerender({ format: ExportApiFormatKey.ris }); + expect(result.current.state.context.params.format).toBe(ExportApiFormatKey.ris); + }); +}); diff --git a/src/components/CitationExporter/useCitationExporter.ts b/src/components/CitationExporter/useCitationExporter.ts index 4501ba195..d4e9e0aff 100644 --- a/src/components/CitationExporter/useCitationExporter.ts +++ b/src/components/CitationExporter/useCitationExporter.ts @@ -82,12 +82,14 @@ export const useCitationExporter = ({ // trigger updates to machine state if incoming props change useEffect(() => dispatch({ type: 'SET_SINGLEMODE', payload: singleMode }), [singleMode, dispatch]); - // watch for format changes + // One-way prop -> machine sync. Must depend only on the prop; adding the + // derived machine value (params.format) would cause the effect to re-fire + // after a user-initiated SET_FORMAT and immediately revert it. useEffect(() => { if (format !== params.format) { dispatch({ type: 'SET_FORMAT', payload: format }); } - }, [format, params.format, dispatch]); + }, [format]); // eslint-disable-line react-hooks/exhaustive-deps // if we're in singleMode and format is changed, trigger a submit useEffect(() => { @@ -96,21 +98,21 @@ export const useCitationExporter = ({ } }, [params.format, singleMode, dispatch]); - // watch for changes to records + // One-way prop -> machine sync. See note on the format effect above. useEffect(() => { // naively compare only the first record, this should be enough to determine // there is a difference if (records[0] !== state.context.records[0]) { dispatch({ type: 'SET_RECORDS', payload: records }); } - }, [records, state.context.records, dispatch]); + }, [records]); // eslint-disable-line react-hooks/exhaustive-deps - // watch for changes to sort + // One-way prop -> machine sync. See note on the format effect above. useEffect(() => { if (sort !== params.sort) { dispatch({ type: 'SET_SORT', payload: sort }); } - }, [sort, params.sort, dispatch]); + }, [sort]); // eslint-disable-line react-hooks/exhaustive-deps // main result fetcher, this will not run unless we're in the 'fetching' state const result = useGetExportCitation(params, {