use-page-create.spec.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import type { ItemInstance } from '@headless-tree/core';
  2. import { fireEvent, render, screen } from '@testing-library/react';
  3. import type { IPageForItem } from '~/interfaces/page';
  4. import { resetCreatingFlagForTesting } from '../states/_inner/page-tree-create';
  5. import { CreateButtonInner } from './use-page-create';
  6. // Mock the dependencies
  7. vi.mock('~/client/components/NotAvailableForGuest', () => ({
  8. NotAvailableForGuest: ({ children }: { children: React.ReactNode }) => (
  9. <>{children}</>
  10. ),
  11. }));
  12. vi.mock('~/client/components/NotAvailableForReadOnlyUser', () => ({
  13. NotAvailableForReadOnlyUser: ({
  14. children,
  15. }: {
  16. children: React.ReactNode;
  17. }) => <>{children}</>,
  18. }));
  19. // Mock useCreatingParentId to control isCreating state
  20. const mockUseCreatingParentId = vi.fn<() => string | null>(() => null);
  21. vi.mock('../states/_inner', () => ({
  22. useCreatingParentId: () => mockUseCreatingParentId(),
  23. usePageTreeCreateActions: vi.fn(() => ({
  24. startCreating: vi.fn(),
  25. cancelCreating: vi.fn(),
  26. })),
  27. }));
  28. /**
  29. * Create a mock item instance for testing
  30. */
  31. const createMockItem = (
  32. id: string,
  33. path: string = '/test/path',
  34. ): ItemInstance<IPageForItem> => {
  35. return {
  36. getId: () => id,
  37. getItemData: () => ({ _id: id, path }) as IPageForItem,
  38. } as unknown as ItemInstance<IPageForItem>;
  39. };
  40. describe('CreateButtonInner', () => {
  41. beforeEach(() => {
  42. resetCreatingFlagForTesting();
  43. mockUseCreatingParentId.mockReturnValue(null);
  44. });
  45. describe('rendering', () => {
  46. test('should render button for regular page', () => {
  47. const mockItem = createMockItem('page-id', '/regular/path');
  48. const onStartCreating = vi.fn();
  49. render(
  50. <CreateButtonInner item={mockItem} onStartCreating={onStartCreating} />,
  51. );
  52. expect(screen.getByRole('button')).toBeInTheDocument();
  53. });
  54. test('should NOT render button for users top page', () => {
  55. const mockItem = createMockItem('users-top', '/user');
  56. const onStartCreating = vi.fn();
  57. render(
  58. <CreateButtonInner item={mockItem} onStartCreating={onStartCreating} />,
  59. );
  60. expect(screen.queryByRole('button')).not.toBeInTheDocument();
  61. });
  62. });
  63. describe('onMouseDown behavior', () => {
  64. test('should call preventDefault on mousedown to prevent focus change', () => {
  65. const mockItem = createMockItem('page-id');
  66. const onStartCreating = vi.fn();
  67. render(
  68. <CreateButtonInner item={mockItem} onStartCreating={onStartCreating} />,
  69. );
  70. const button = screen.getByRole('button');
  71. // Use fireEvent which triggers React's synthetic event handler
  72. // and check if the event was prevented by examining defaultPrevented
  73. const mouseDownEvent = fireEvent.mouseDown(button);
  74. // fireEvent returns false if preventDefault was called
  75. // (the event's default action was prevented)
  76. expect(mouseDownEvent).toBe(false);
  77. });
  78. test('should preserve focus on input when clicking CreateButton', () => {
  79. const mockItem = createMockItem('page-id');
  80. const onStartCreating = vi.fn();
  81. render(
  82. <div>
  83. <input data-testid="placeholder-input" />
  84. <CreateButtonInner
  85. item={mockItem}
  86. onStartCreating={onStartCreating}
  87. />
  88. </div>,
  89. );
  90. const input = screen.getByTestId('placeholder-input');
  91. const button = screen.getByRole('button');
  92. // Focus the input
  93. input.focus();
  94. expect(document.activeElement).toBe(input);
  95. // mousedown on button - React's onMouseDown with preventDefault should fire
  96. const mouseDownEvent = fireEvent.mouseDown(button);
  97. // Verify preventDefault was called
  98. expect(mouseDownEvent).toBe(false);
  99. // Note: jsdom doesn't fully simulate focus behavior with preventDefault,
  100. // but we've verified preventDefault is called
  101. });
  102. });
  103. describe('onClick behavior', () => {
  104. test('should call onStartCreating with item when not already creating', () => {
  105. const mockItem = createMockItem('page-id');
  106. const onStartCreating = vi.fn();
  107. // isCreating = false (creatingParentId is null)
  108. mockUseCreatingParentId.mockReturnValue(null);
  109. render(
  110. <CreateButtonInner item={mockItem} onStartCreating={onStartCreating} />,
  111. );
  112. const button = screen.getByRole('button');
  113. fireEvent.click(button);
  114. expect(onStartCreating).toHaveBeenCalledTimes(1);
  115. expect(onStartCreating).toHaveBeenCalledWith(mockItem);
  116. });
  117. test('should NOT call onStartCreating when already creating', () => {
  118. const mockItem = createMockItem('page-id');
  119. const onStartCreating = vi.fn();
  120. // isCreating = true (creatingParentId is not null)
  121. mockUseCreatingParentId.mockReturnValue('some-parent-id');
  122. render(
  123. <CreateButtonInner item={mockItem} onStartCreating={onStartCreating} />,
  124. );
  125. const button = screen.getByRole('button');
  126. fireEvent.click(button);
  127. expect(onStartCreating).not.toHaveBeenCalled();
  128. });
  129. test('should call stopPropagation on click to prevent parent handlers', () => {
  130. const mockItem = createMockItem('page-id');
  131. const onStartCreating = vi.fn();
  132. const parentClickHandler = vi.fn();
  133. render(
  134. // biome-ignore lint/a11y/noStaticElementInteractions: ignore
  135. // biome-ignore lint/a11y/useKeyWithClickEvents: ignore
  136. <div onClick={parentClickHandler}>
  137. <CreateButtonInner
  138. item={mockItem}
  139. onStartCreating={onStartCreating}
  140. />
  141. </div>,
  142. );
  143. const button = screen.getByRole('button');
  144. fireEvent.click(button);
  145. // Parent should not receive the click event
  146. expect(parentClickHandler).not.toHaveBeenCalled();
  147. });
  148. });
  149. describe('rapid click prevention', () => {
  150. test('should ignore clicks when already in creating mode', () => {
  151. const mockItem = createMockItem('page-id');
  152. const onStartCreating = vi.fn();
  153. // Start with not creating
  154. mockUseCreatingParentId.mockReturnValue(null);
  155. const { rerender } = render(
  156. <CreateButtonInner item={mockItem} onStartCreating={onStartCreating} />,
  157. );
  158. const button = screen.getByRole('button');
  159. // First click should work
  160. fireEvent.click(button);
  161. expect(onStartCreating).toHaveBeenCalledTimes(1);
  162. // Simulate that creating mode is now active
  163. mockUseCreatingParentId.mockReturnValue('page-id');
  164. rerender(
  165. <CreateButtonInner item={mockItem} onStartCreating={onStartCreating} />,
  166. );
  167. // Second click should be ignored
  168. fireEvent.click(button);
  169. expect(onStartCreating).toHaveBeenCalledTimes(1);
  170. });
  171. });
  172. });