|
|
@@ -11,29 +11,38 @@ vi.mock('next/router', () => ({
|
|
|
useRouter: vi.fn(() => mockRouter),
|
|
|
}));
|
|
|
|
|
|
-// 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(),
|
|
|
+// Mock dependencies - only mock fetchCurrentPage to test the integration
|
|
|
+const mockFetchCurrentPage = vi.fn();
|
|
|
+const mockMutateEditingMarkdown = vi.fn();
|
|
|
+
|
|
|
+vi.mock('~/states/page', async() => ({
|
|
|
+ // Use real implementations for state hooks to get closer to actual behavior
|
|
|
+ ...(await vi.importActual('~/states/page')),
|
|
|
+ // Only mock the fetch function since that's what we want to test
|
|
|
+ useFetchCurrentPage: () => ({ fetchCurrentPage: mockFetchCurrentPage }),
|
|
|
}));
|
|
|
|
|
|
vi.mock('~/stores/editor', () => ({
|
|
|
- useEditingMarkdown: () => mockUseEditingMarkdown(),
|
|
|
+ useEditingMarkdown: () => ({ mutate: mockMutateEditingMarkdown }),
|
|
|
}));
|
|
|
|
|
|
describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
- // Mock functions
|
|
|
- const mockSetCurrentPageId = vi.fn();
|
|
|
- const mockFetchCurrentPage = vi.fn();
|
|
|
- const mockMutateEditingMarkdown = vi.fn();
|
|
|
+ // Create realistic page data mock
|
|
|
+ const createPageDataMock = (pageId: string, path: string, body: string) => ({
|
|
|
+ _id: pageId,
|
|
|
+ path,
|
|
|
+ revision: {
|
|
|
+ _id: `rev_${pageId}`,
|
|
|
+ body,
|
|
|
+ author: { _id: 'user1', name: 'Test User' },
|
|
|
+ createdAt: new Date(),
|
|
|
+ },
|
|
|
+ creator: { _id: 'user1', name: 'Test User' },
|
|
|
+ lastUpdateUser: { _id: 'user1', name: 'Test User' },
|
|
|
+ grant: 1, // GRANT_PUBLIC
|
|
|
+ createdAt: new Date(),
|
|
|
+ updatedAt: new Date(),
|
|
|
+ });
|
|
|
|
|
|
// Test helper to create props
|
|
|
const createProps = (currentPathname: string) => {
|
|
|
@@ -48,17 +57,9 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
mockRouter.pathname = '/[[...path]]';
|
|
|
(useRouter as ReturnType<typeof vi.fn>).mockReturnValue(mockRouter);
|
|
|
|
|
|
- // Base hook returns
|
|
|
- mockUseCurrentPageData.mockReturnValue([null]);
|
|
|
- mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
- mockUseFetchCurrentPage.mockReturnValue({ fetchCurrentPage: mockFetchCurrentPage });
|
|
|
- mockUseEditingMarkdown.mockReturnValue({ mutate: mockMutateEditingMarkdown });
|
|
|
- mockUseCurrentPageLoading.mockReturnValue({ isLoading: false, error: null });
|
|
|
-
|
|
|
- // Default fetch behavior
|
|
|
- mockFetchCurrentPage.mockResolvedValue({
|
|
|
- revision: { body: 'fetched content' },
|
|
|
- });
|
|
|
+ // Default fetch behavior with realistic page data
|
|
|
+ const defaultPageData = createPageDataMock('page123', '/current/path', 'fetched content');
|
|
|
+ mockFetchCurrentPage.mockResolvedValue(defaultPageData);
|
|
|
});
|
|
|
|
|
|
describe('CORE FIX: router.asPath dependency', () => {
|
|
|
@@ -78,56 +79,41 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
expect(mockFetchCurrentPage).not.toHaveBeenCalledWith('/stale/props/path');
|
|
|
});
|
|
|
|
|
|
- it('should react to router.asPath changes (useEffect dependency)', async() => {
|
|
|
- const props = createProps('/some/path');
|
|
|
+ it('should always fetch when navigating to check for content changes', async() => {
|
|
|
+ // This test exposes the core bug: not fetching when we should
|
|
|
+ const props = createProps('/same/path');
|
|
|
+ mockRouter.asPath = '/same/path';
|
|
|
|
|
|
- // Start with mismatched state to trigger initial fetch
|
|
|
- mockRouter.asPath = '/some/path';
|
|
|
- mockUseCurrentPageData.mockReturnValue([{ path: '/different/path' }]);
|
|
|
- mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
+ // Set up different page data to simulate the scenario where:
|
|
|
+ // - Current loaded page has different content than what should be loaded
|
|
|
+ // - Same path but different page ID/content
|
|
|
|
|
|
- const { rerender } = renderHook(() => useSameRouteNavigation(props));
|
|
|
+ const currentPageData = createPageDataMock('oldPageId', '/same/path', 'Old content');
|
|
|
+ const newPageData = createPageDataMock('newPageId', '/same/path', 'New content');
|
|
|
|
|
|
- // Initial render should trigger fetch
|
|
|
- await act(async() => {
|
|
|
- await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
- });
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenCalledTimes(1);
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenLastCalledWith('/some/path');
|
|
|
-
|
|
|
- mockFetchCurrentPage.mockClear();
|
|
|
+ mockFetchCurrentPage.mockResolvedValue(newPageData);
|
|
|
|
|
|
- // Browser navigation changes router.asPath
|
|
|
- mockRouter.asPath = '/new/browser/location';
|
|
|
- rerender();
|
|
|
+ renderHook(() => useSameRouteNavigation(props));
|
|
|
|
|
|
await act(async() => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
|
|
|
- // Should detect the change and fetch new content
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenCalledTimes(1);
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenLastCalledWith('/new/browser/location');
|
|
|
+ // CRITICAL: Should always fetch to ensure we have the latest content
|
|
|
+ // This will currently fail because the implementation doesn't fetch
|
|
|
+ // when it thinks the page is already loaded for the same path
|
|
|
+ expect(mockFetchCurrentPage).toHaveBeenCalledWith('/same/path');
|
|
|
+ expect(mockMutateEditingMarkdown).toHaveBeenCalledWith('New content');
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe('Essential behavior verification', () => {
|
|
|
- it('should update currentPageId when navigating', async() => {
|
|
|
+ it('should fetch and update editor content', async() => {
|
|
|
const props = createProps('/some/page');
|
|
|
mockRouter.asPath = '/some/page';
|
|
|
|
|
|
- renderHook(() => useSameRouteNavigation(props));
|
|
|
-
|
|
|
- await act(async() => {
|
|
|
- await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
- });
|
|
|
-
|
|
|
- expect(mockSetCurrentPageId).toHaveBeenCalledWith(undefined);
|
|
|
- });
|
|
|
-
|
|
|
- it('should update editor content on successful fetch', async() => {
|
|
|
- const props = createProps('/page/path');
|
|
|
- mockRouter.asPath = '/page/path';
|
|
|
+ const pageData = createPageDataMock('page456', '/some/page', 'page content');
|
|
|
+ mockFetchCurrentPage.mockResolvedValue(pageData);
|
|
|
|
|
|
renderHook(() => useSameRouteNavigation(props));
|
|
|
|
|
|
@@ -135,81 +121,46 @@ describe('useSameRouteNavigation - Essential Bug Fix Verification', () => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
|
|
|
- expect(mockMutateEditingMarkdown).toHaveBeenCalledWith('fetched content');
|
|
|
- });
|
|
|
-
|
|
|
- it('should skip fetch when page already matches router state', () => {
|
|
|
- const props = createProps('/current/page');
|
|
|
- mockRouter.asPath = '/current/page';
|
|
|
-
|
|
|
- // Page already loaded and matches
|
|
|
- mockUseCurrentPageData.mockReturnValue([{ path: '/current/page' }]);
|
|
|
- mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
-
|
|
|
- renderHook(() => useSameRouteNavigation(props));
|
|
|
-
|
|
|
- expect(mockFetchCurrentPage).not.toHaveBeenCalled();
|
|
|
+ expect(mockFetchCurrentPage).toHaveBeenCalledWith('/some/page');
|
|
|
+ expect(mockMutateEditingMarkdown).toHaveBeenCalledWith('page content');
|
|
|
});
|
|
|
|
|
|
- // Race condition prevention - core bug fix
|
|
|
- it('should prevent concurrent fetches during rapid navigation', async() => {
|
|
|
+ it('should handle navigation between different pages', async() => {
|
|
|
const props = createProps('/first/page');
|
|
|
mockRouter.asPath = '/first/page';
|
|
|
|
|
|
- // Simulate slow network request
|
|
|
- let resolveFetch: (value: { revision: { body: string } }) => void = () => {};
|
|
|
- const slowFetchPromise = new Promise((resolve) => {
|
|
|
- resolveFetch = resolve;
|
|
|
- });
|
|
|
- mockFetchCurrentPage.mockReturnValue(slowFetchPromise);
|
|
|
+ // First page data
|
|
|
+ const firstPageData = createPageDataMock('page1', '/first/page', 'First page content');
|
|
|
+ mockFetchCurrentPage.mockResolvedValue(firstPageData);
|
|
|
|
|
|
const { rerender } = renderHook(() => useSameRouteNavigation(props));
|
|
|
|
|
|
- // Start first fetch
|
|
|
await act(async() => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenCalledTimes(1);
|
|
|
|
|
|
- // Rapid navigation - should be prevented
|
|
|
- mockRouter.asPath = '/second/page';
|
|
|
- rerender();
|
|
|
+ expect(mockFetchCurrentPage).toHaveBeenCalledWith('/first/page');
|
|
|
+ expect(mockMutateEditingMarkdown).toHaveBeenCalledWith('First page content');
|
|
|
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenCalledTimes(1);
|
|
|
+ // Clear mocks and navigate to second page
|
|
|
+ mockFetchCurrentPage.mockClear();
|
|
|
+ mockMutateEditingMarkdown.mockClear();
|
|
|
|
|
|
- // Complete the first fetch
|
|
|
- resolveFetch({ revision: { body: 'content' } });
|
|
|
- await act(async() => {
|
|
|
- await slowFetchPromise;
|
|
|
- });
|
|
|
+ // Second page data - different ID and content
|
|
|
+ const secondPageData = createPageDataMock('page2', '/second/page', 'Second page content');
|
|
|
+ mockFetchCurrentPage.mockResolvedValue(secondPageData);
|
|
|
|
|
|
- // Now second navigation should work
|
|
|
- mockRouter.asPath = '/third/page';
|
|
|
+ // Simulate navigation to different page
|
|
|
+ mockRouter.asPath = '/second/page';
|
|
|
rerender();
|
|
|
|
|
|
await act(async() => {
|
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
});
|
|
|
|
|
|
- expect(mockFetchCurrentPage).toHaveBeenCalledTimes(2);
|
|
|
- });
|
|
|
-
|
|
|
- // State clearing sequence - prevents stale data
|
|
|
- it('should clear pageId before setting new one', async() => {
|
|
|
- const props = createProps('/new/page');
|
|
|
- mockRouter.asPath = '/new/page';
|
|
|
-
|
|
|
- mockUseCurrentPageId.mockReturnValue([null, mockSetCurrentPageId]);
|
|
|
-
|
|
|
- renderHook(() => useSameRouteNavigation(props));
|
|
|
-
|
|
|
- await act(async() => {
|
|
|
- await new Promise(resolve => setTimeout(resolve, 0));
|
|
|
- });
|
|
|
-
|
|
|
- expect(mockSetCurrentPageId).toHaveBeenCalledWith(undefined);
|
|
|
- expect(mockSetCurrentPageId).toHaveBeenCalledTimes(1);
|
|
|
- expect(mockSetCurrentPageId.mock.calls[0][0]).toBe(undefined);
|
|
|
+ // Should fetch new page and update content
|
|
|
+ expect(mockFetchCurrentPage).toHaveBeenCalledWith('/second/page');
|
|
|
+ expect(mockMutateEditingMarkdown).toHaveBeenCalledWith('Second page content');
|
|
|
});
|
|
|
});
|
|
|
});
|