|
|
@@ -5,26 +5,24 @@ import { mockDeep } from 'vitest-mock-extended';
|
|
|
|
|
|
import { useSameRouteNavigation } from './use-same-route-navigation';
|
|
|
|
|
|
-// Mock Next.js router first
|
|
|
+// Mock Next.js router
|
|
|
const mockRouter = mockDeep<NextRouter>();
|
|
|
vi.mock('next/router', () => ({
|
|
|
useRouter: vi.fn(() => mockRouter),
|
|
|
}));
|
|
|
|
|
|
-// Mock other dependencies
|
|
|
-vi.mock('~/states/page');
|
|
|
-vi.mock('~/stores/editor');
|
|
|
-
|
|
|
-// Mock hook implementations
|
|
|
+// Mock dependencies and their implementations
|
|
|
const mockUseCurrentPageData = vi.fn();
|
|
|
const mockUseCurrentPageId = vi.fn();
|
|
|
const mockUseFetchCurrentPage = vi.fn();
|
|
|
const mockUseEditingMarkdown = vi.fn();
|
|
|
+const mockUseCurrentPageLoading = vi.fn();
|
|
|
|
|
|
vi.mock('~/states/page', () => ({
|
|
|
useCurrentPageData: () => mockUseCurrentPageData(),
|
|
|
useCurrentPageId: () => mockUseCurrentPageId(),
|
|
|
useFetchCurrentPage: () => mockUseFetchCurrentPage(),
|
|
|
+ useCurrentPageLoading: () => mockUseCurrentPageLoading(),
|
|
|
}));
|
|
|
|
|
|
vi.mock('~/stores/editor', () => ({
|
|
|
@@ -37,16 +35,15 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
const mockFetchCurrentPage = vi.fn();
|
|
|
const mockMutateEditingMarkdown = vi.fn();
|
|
|
|
|
|
- // Simple props simulation - enough to trigger the actual hook logic
|
|
|
+ // Test helper to create props
|
|
|
const createProps = (currentPathname: string) => {
|
|
|
- // Cast to bypass type complexity - focus on behavior testing
|
|
|
return { currentPathname } as Parameters<typeof useSameRouteNavigation>[0];
|
|
|
};
|
|
|
|
|
|
beforeEach(() => {
|
|
|
vi.clearAllMocks();
|
|
|
|
|
|
- // Base router setup - tests will override as needed
|
|
|
+ // Base router setup
|
|
|
mockRouter.asPath = '/current/path';
|
|
|
mockRouter.pathname = '/[[...path]]';
|
|
|
(useRouter as ReturnType<typeof vi.fn>).mockReturnValue(mockRouter);
|
|
|
@@ -56,6 +53,7 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
mockUseFetchCurrentPage.mockReturnValue({ fetchCurrentPage: mockFetchCurrentPage });
|
|
|
mockUseEditingMarkdown.mockReturnValue({ mutate: mockMutateEditingMarkdown });
|
|
|
+ mockUseCurrentPageLoading.mockReturnValue({ isLoading: false, error: null });
|
|
|
|
|
|
// Default fetch behavior
|
|
|
mockFetchCurrentPage.mockResolvedValue({
|
|
|
@@ -65,9 +63,9 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
|
|
|
describe('CORE FIX: router.asPath dependency', () => {
|
|
|
it('should fetch when router.asPath differs from props.currentPathname', async() => {
|
|
|
- // THE EXACT BUG SCENARIO: stale props vs current router state
|
|
|
+ // Bug scenario: stale props vs current router state
|
|
|
const props = createProps('/stale/props/path');
|
|
|
- mockRouter.asPath = '/actual/browser/path'; // Browser is actually here
|
|
|
+ mockRouter.asPath = '/actual/browser/path';
|
|
|
|
|
|
renderHook(() => useSameRouteNavigation(props));
|
|
|
|
|
|
@@ -75,7 +73,7 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
|
|
|
- // VERIFY THE FIX: should use router.asPath, not props.currentPathname
|
|
|
+ // Verify fix: should use router.asPath, not props.currentPathname
|
|
|
expect(mockFetchCurrentPage).toHaveBeenCalledWith('/actual/browser/path');
|
|
|
expect(mockFetchCurrentPage).not.toHaveBeenCalledWith('/stale/props/path');
|
|
|
});
|
|
|
@@ -83,21 +81,20 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
it('should react to router.asPath changes (useEffect dependency)', async() => {
|
|
|
const props = createProps('/some/path');
|
|
|
|
|
|
- // Start with MISMATCHED state to trigger initial fetch
|
|
|
+ // Start with mismatched state to trigger initial fetch
|
|
|
mockRouter.asPath = '/some/path';
|
|
|
mockUseCurrentPageData.mockReturnValue([{ path: '/different/path' }]);
|
|
|
- mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]); // null for non-permalink
|
|
|
+ mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
|
|
|
const { rerender } = renderHook(() => useSameRouteNavigation(props));
|
|
|
|
|
|
- // Initial render should trigger fetch (mismatched state)
|
|
|
+ // Initial render should trigger fetch
|
|
|
await act(async() => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
expect(mockFetchCurrentPage).toHaveBeenCalledTimes(1);
|
|
|
expect(mockFetchCurrentPage).toHaveBeenLastCalledWith('/some/path');
|
|
|
|
|
|
- // Clear the mock to test the change detection
|
|
|
mockFetchCurrentPage.mockClear();
|
|
|
|
|
|
// Browser navigation changes router.asPath
|
|
|
@@ -126,10 +123,6 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
});
|
|
|
|
|
|
expect(mockSetCurrentPageId).toHaveBeenCalledWith(undefined);
|
|
|
- // Since extractPageIdFromPathname returns null for non-permalink paths,
|
|
|
- // but the hook uses: if (targetPageId) { setCurrentPageId(targetPageId); }
|
|
|
- // When targetPageId is null, setCurrentPageId is not called with null
|
|
|
- // Only the clearing call with undefined happens
|
|
|
});
|
|
|
|
|
|
it('should update editor content on successful fetch', async() => {
|
|
|
@@ -151,15 +144,14 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
|
|
|
// Page already loaded and matches
|
|
|
mockUseCurrentPageData.mockReturnValue([{ path: '/current/page' }]);
|
|
|
- mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]); // null for non-permalink
|
|
|
+ mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
|
|
|
renderHook(() => useSameRouteNavigation(props));
|
|
|
|
|
|
- // Should not fetch when everything is already in sync
|
|
|
expect(mockFetchCurrentPage).not.toHaveBeenCalled();
|
|
|
});
|
|
|
|
|
|
- // CRITICAL: Race condition prevention - THE CORE BUG FIX
|
|
|
+ // Race condition prevention - core bug fix
|
|
|
it('should prevent concurrent fetches during rapid navigation', async() => {
|
|
|
const props = createProps('/first/page');
|
|
|
mockRouter.asPath = '/first/page';
|
|
|
@@ -183,8 +175,7 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
mockRouter.asPath = '/second/page';
|
|
|
rerender();
|
|
|
|
|
|
- // Should NOT start second fetch due to race condition protection
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenCalledTimes(1); // Still only 1 call
|
|
|
+ expect(mockFetchCurrentPage).toHaveBeenCalledTimes(1);
|
|
|
|
|
|
// Complete the first fetch
|
|
|
resolveFetch({ revision: { body: 'content' } });
|
|
|
@@ -200,16 +191,15 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenCalledTimes(2); // Now second call
|
|
|
+ expect(mockFetchCurrentPage).toHaveBeenCalledTimes(2);
|
|
|
});
|
|
|
|
|
|
- // CRITICAL: State clearing sequence - prevents stale data
|
|
|
+ // State clearing sequence - prevents stale data
|
|
|
it('should clear pageId before setting new one', async() => {
|
|
|
const props = createProps('/new/page');
|
|
|
mockRouter.asPath = '/new/page';
|
|
|
|
|
|
- // Start with existing page
|
|
|
- mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]); // null for non-permalink
|
|
|
+ mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
|
|
|
renderHook(() => useSameRouteNavigation(props));
|
|
|
|
|
|
@@ -217,10 +207,8 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
|
|
|
- // Verify clearing sequence: undefined first, but no second call for null
|
|
|
- // because the hook only calls setCurrentPageId(targetPageId) if targetPageId is truthy
|
|
|
expect(mockSetCurrentPageId).toHaveBeenCalledWith(undefined);
|
|
|
- expect(mockSetCurrentPageId).toHaveBeenCalledTimes(1); // Only the clear call
|
|
|
+ expect(mockSetCurrentPageId).toHaveBeenCalledTimes(1);
|
|
|
expect(mockSetCurrentPageId.mock.calls[0][0]).toBe(undefined);
|
|
|
});
|
|
|
});
|