Przeglądaj źródła

test: add placeholder node tests for page creation in tree

Add comprehensive tests to verify placeholder node functionality:
- Test that placeholder is prepended when parent is in creating mode
- Test that placeholder is not added to different parent
- Test that placeholder works with empty parent (no existing children)
- Test that creating state is read via refs after state change
- Test that dataLoader reference remains stable when creating state changes

These tests ensure the page creation input works correctly in the tree.
Yuki Takei 4 miesięcy temu
rodzic
commit
359ade4468

+ 3 - 0
apps/app/src/features/page-tree/client/components/SimplifiedItemsTree.spec.tsx

@@ -360,4 +360,7 @@ describe('SimplifiedItemsTree', () => {
       document.body.removeChild(scrollerElem);
     });
   });
+
+  // NOTE: Page creation placeholder tests are covered in use-data-loader.spec.tsx
+  // The dataLoader is responsible for prepending placeholder nodes when creatingParentId is set
 });

+ 149 - 2
apps/app/src/features/page-tree/client/hooks/use-data-loader.spec.tsx

@@ -20,13 +20,17 @@ vi.mock('~/client/util/apiv3-client', () => ({
   apiv3Get: (...args: unknown[]) => mockApiv3Get(...args),
 }));
 
+// Mutable state for creating parent info
+let mockCreatingParentId: string | null = null;
+let mockCreatingParentPath: string | null = null;
+
 // Mock the page-tree-create state hooks
 vi.mock('../states/page-tree-create', async () => {
   const actual = await vi.importActual('../states/page-tree-create');
   return {
     ...actual,
-    useCreatingParentId: () => null,
-    useCreatingParentPath: () => null,
+    useCreatingParentId: () => mockCreatingParentId,
+    useCreatingParentPath: () => mockCreatingParentPath,
   };
 });
 
@@ -66,6 +70,9 @@ describe('use-data-loader', () => {
     clearChildrenCache();
     // Reset mock
     mockApiv3Get.mockReset();
+    // Reset creating state
+    mockCreatingParentId = null;
+    mockCreatingParentPath = null;
   });
 
   describe('useDataLoader', () => {
@@ -413,4 +420,144 @@ describe('use-data-loader', () => {
       expect(mockApiv3Get).toHaveBeenCalledTimes(4);
     });
   });
+
+  describe('placeholder node for page creation', () => {
+    test('should prepend placeholder node when parent is in creating mode', async () => {
+      const mockChildren = [createMockPage('existing-child', '/parent/existing')];
+      mockApiv3Get.mockResolvedValue({ data: { children: mockChildren } });
+
+      // Set creating state BEFORE rendering the hook
+      mockCreatingParentId = 'parent-id';
+      mockCreatingParentPath = '/parent';
+
+      const { result } = renderHook(() =>
+        useDataLoader(ROOT_PAGE_ID, ALL_PAGES_COUNT),
+      );
+
+      const children = await getDataLoader(result).getChildrenWithData('parent-id');
+
+      // Should have placeholder + existing children
+      expect(children).toHaveLength(2);
+      // Placeholder should be first
+      expect(children[0].id).toBe(CREATING_PAGE_VIRTUAL_ID);
+      expect(children[0].data._id).toBe(CREATING_PAGE_VIRTUAL_ID);
+      expect(children[0].data.parent).toBe('parent-id');
+      expect(children[0].data.path).toBe('/parent/');
+      // Existing child should be second
+      expect(children[1].id).toBe('existing-child');
+    });
+
+    test('should not add placeholder when parent is not in creating mode', async () => {
+      const mockChildren = [createMockPage('existing-child', '/parent/existing')];
+      mockApiv3Get.mockResolvedValue({ data: { children: mockChildren } });
+
+      // Creating state is null (not creating)
+      mockCreatingParentId = null;
+      mockCreatingParentPath = null;
+
+      const { result } = renderHook(() =>
+        useDataLoader(ROOT_PAGE_ID, ALL_PAGES_COUNT),
+      );
+
+      const children = await getDataLoader(result).getChildrenWithData('parent-id');
+
+      // Should only have existing children, no placeholder
+      expect(children).toHaveLength(1);
+      expect(children[0].id).toBe('existing-child');
+    });
+
+    test('should not add placeholder to different parent', async () => {
+      const mockChildren = [createMockPage('existing-child', '/other/existing')];
+      mockApiv3Get.mockResolvedValue({ data: { children: mockChildren } });
+
+      // Creating under 'parent-id', but fetching children of 'other-id'
+      mockCreatingParentId = 'parent-id';
+      mockCreatingParentPath = '/parent';
+
+      const { result } = renderHook(() =>
+        useDataLoader(ROOT_PAGE_ID, ALL_PAGES_COUNT),
+      );
+
+      const children = await getDataLoader(result).getChildrenWithData('other-id');
+
+      // Should only have existing children, no placeholder
+      expect(children).toHaveLength(1);
+      expect(children[0].id).toBe('existing-child');
+    });
+
+    test('should add placeholder to empty parent (no existing children)', async () => {
+      // Parent has no existing children
+      mockApiv3Get.mockResolvedValue({ data: { children: [] } });
+
+      // Set creating state
+      mockCreatingParentId = 'empty-parent-id';
+      mockCreatingParentPath = '/empty-parent';
+
+      const { result } = renderHook(() =>
+        useDataLoader(ROOT_PAGE_ID, ALL_PAGES_COUNT),
+      );
+
+      const children = await getDataLoader(result).getChildrenWithData('empty-parent-id');
+
+      // Should have only the placeholder
+      expect(children).toHaveLength(1);
+      expect(children[0].id).toBe(CREATING_PAGE_VIRTUAL_ID);
+      expect(children[0].data.parent).toBe('empty-parent-id');
+      expect(children[0].data.path).toBe('/empty-parent/');
+    });
+
+    test('should read creating state via refs when called after state change', async () => {
+      const mockChildren = [createMockPage('existing-child', '/parent/existing')];
+      mockApiv3Get.mockResolvedValue({ data: { children: mockChildren } });
+
+      // Render hook WITHOUT creating state
+      const { result, rerender } = renderHook(() =>
+        useDataLoader(ROOT_PAGE_ID, ALL_PAGES_COUNT),
+      );
+
+      // First call - no placeholder
+      clearChildrenCache();
+      const childrenBefore = await getDataLoader(result).getChildrenWithData('parent-id');
+      expect(childrenBefore).toHaveLength(1);
+      expect(childrenBefore[0].id).toBe('existing-child');
+
+      // Now set creating state
+      mockCreatingParentId = 'parent-id';
+      mockCreatingParentPath = '/parent';
+
+      // Rerender to update refs
+      rerender();
+
+      // Clear cache to force re-fetch
+      clearChildrenCache();
+
+      // Second call - should have placeholder because refs are updated
+      const childrenAfter = await getDataLoader(result).getChildrenWithData('parent-id');
+      expect(childrenAfter).toHaveLength(2);
+      expect(childrenAfter[0].id).toBe(CREATING_PAGE_VIRTUAL_ID);
+      expect(childrenAfter[1].id).toBe('existing-child');
+    });
+
+    test('dataLoader reference should remain stable when creating state changes', () => {
+      // Render hook WITHOUT creating state
+      const { result, rerender } = renderHook(() =>
+        useDataLoader(ROOT_PAGE_ID, ALL_PAGES_COUNT),
+      );
+
+      const firstDataLoader = result.current;
+
+      // Change creating state
+      mockCreatingParentId = 'some-parent';
+      mockCreatingParentPath = '/some-parent';
+
+      // Rerender
+      rerender();
+
+      const secondDataLoader = result.current;
+
+      // DataLoader reference should be STABLE (same reference)
+      // This is critical to prevent headless-tree from refetching all data
+      expect(firstDataLoader).toBe(secondDataLoader);
+    });
+  });
 });