You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import React from 'react'
  2. import { render, screen } from '@testing-library/react'
  3. import '@testing-library/jest-dom'
  4. import NavLink from './navLink'
  5. import type { NavLinkProps } from './navLink'
  6. // Mock Next.js navigation
  7. jest.mock('next/navigation', () => ({
  8. useSelectedLayoutSegment: () => 'overview',
  9. }))
  10. // Mock Next.js Link component
  11. jest.mock('next/link', () => {
  12. return function MockLink({ children, href, className, title }: any) {
  13. return (
  14. <a href={href} className={className} title={title} data-testid="nav-link">
  15. {children}
  16. </a>
  17. )
  18. }
  19. })
  20. // Mock RemixIcon components
  21. const MockIcon = ({ className }: { className?: string }) => (
  22. <svg className={className} data-testid="nav-icon" />
  23. )
  24. describe('NavLink Text Animation Issues', () => {
  25. const mockProps: NavLinkProps = {
  26. name: 'Orchestrate',
  27. href: '/app/123/workflow',
  28. iconMap: {
  29. selected: MockIcon,
  30. normal: MockIcon,
  31. },
  32. }
  33. beforeEach(() => {
  34. // Mock getComputedStyle for transition testing
  35. Object.defineProperty(window, 'getComputedStyle', {
  36. value: jest.fn((element) => {
  37. const isExpanded = element.getAttribute('data-mode') === 'expand'
  38. return {
  39. transition: 'all 0.3s ease',
  40. opacity: isExpanded ? '1' : '0',
  41. width: isExpanded ? 'auto' : '0px',
  42. overflow: 'hidden',
  43. paddingLeft: isExpanded ? '12px' : '10px', // px-3 vs px-2.5
  44. paddingRight: isExpanded ? '12px' : '10px',
  45. }
  46. }),
  47. writable: true,
  48. })
  49. })
  50. describe('Text Squeeze Animation Issue', () => {
  51. it('should show text squeeze effect when switching from collapse to expand', async () => {
  52. const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
  53. // In collapse mode, text should be in DOM but hidden via CSS
  54. const textElement = screen.getByText('Orchestrate')
  55. expect(textElement).toBeInTheDocument()
  56. expect(textElement).toHaveClass('opacity-0')
  57. expect(textElement).toHaveClass('w-0')
  58. expect(textElement).toHaveClass('overflow-hidden')
  59. // Icon should still be present
  60. expect(screen.getByTestId('nav-icon')).toBeInTheDocument()
  61. // Check padding in collapse mode
  62. const linkElement = screen.getByTestId('nav-link')
  63. expect(linkElement).toHaveClass('px-2.5')
  64. // Switch to expand mode - this is where the squeeze effect occurs
  65. rerender(<NavLink {...mockProps} mode="expand" />)
  66. // Text should now appear
  67. expect(screen.getByText('Orchestrate')).toBeInTheDocument()
  68. // Check padding change - this contributes to the squeeze effect
  69. expect(linkElement).toHaveClass('px-3')
  70. // The bug: text appears abruptly without smooth transition
  71. // This test documents the current behavior that causes the squeeze effect
  72. const expandedTextElement = screen.getByText('Orchestrate')
  73. expect(expandedTextElement).toBeInTheDocument()
  74. // In a properly animated version, we would expect:
  75. // - Opacity transition from 0 to 1
  76. // - Width transition from 0 to auto
  77. // - No layout shift from padding changes
  78. })
  79. it('should maintain icon position consistency during text appearance', () => {
  80. const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
  81. const iconElement = screen.getByTestId('nav-icon')
  82. const initialIconClasses = iconElement.className
  83. // Icon should have mr-0 in collapse mode
  84. expect(iconElement).toHaveClass('mr-0')
  85. rerender(<NavLink {...mockProps} mode="expand" />)
  86. const expandedIconClasses = iconElement.className
  87. // Icon should have mr-2 in expand mode - this shift contributes to the squeeze effect
  88. expect(iconElement).toHaveClass('mr-2')
  89. console.log('Collapsed icon classes:', initialIconClasses)
  90. console.log('Expanded icon classes:', expandedIconClasses)
  91. // This margin change causes the icon to shift when text appears
  92. })
  93. it('should document the abrupt text rendering issue', () => {
  94. const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
  95. // Text is present in DOM but hidden via CSS classes
  96. const collapsedText = screen.getByText('Orchestrate')
  97. expect(collapsedText).toBeInTheDocument()
  98. expect(collapsedText).toHaveClass('opacity-0')
  99. expect(collapsedText).toHaveClass('pointer-events-none')
  100. rerender(<NavLink {...mockProps} mode="expand" />)
  101. // Text suddenly appears in DOM - no transition
  102. expect(screen.getByText('Orchestrate')).toBeInTheDocument()
  103. // The issue: {mode === 'expand' && name} causes abrupt show/hide
  104. // instead of smooth opacity/width transition
  105. })
  106. })
  107. describe('Layout Shift Issues', () => {
  108. it('should detect padding differences causing layout shifts', () => {
  109. const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
  110. const linkElement = screen.getByTestId('nav-link')
  111. // Collapsed state padding
  112. expect(linkElement).toHaveClass('px-2.5')
  113. rerender(<NavLink {...mockProps} mode="expand" />)
  114. // Expanded state padding - different value causes layout shift
  115. expect(linkElement).toHaveClass('px-3')
  116. // This 2px difference (10px vs 12px) contributes to the squeeze effect
  117. })
  118. it('should detect icon margin changes causing shifts', () => {
  119. const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
  120. const iconElement = screen.getByTestId('nav-icon')
  121. // Collapsed: no right margin
  122. expect(iconElement).toHaveClass('mr-0')
  123. rerender(<NavLink {...mockProps} mode="expand" />)
  124. // Expanded: 8px right margin (mr-2)
  125. expect(iconElement).toHaveClass('mr-2')
  126. // This sudden margin appearance causes the squeeze effect
  127. })
  128. })
  129. describe('Active State Handling', () => {
  130. it('should handle active state correctly in both modes', () => {
  131. // Test non-active state
  132. const { rerender } = render(<NavLink {...mockProps} mode="collapse" />)
  133. let linkElement = screen.getByTestId('nav-link')
  134. expect(linkElement).not.toHaveClass('bg-state-accent-active')
  135. // Test with active state (when href matches current segment)
  136. const activeProps = {
  137. ...mockProps,
  138. href: '/app/123/overview', // matches mocked segment
  139. }
  140. rerender(<NavLink {...activeProps} mode="expand" />)
  141. linkElement = screen.getByTestId('nav-link')
  142. expect(linkElement).toHaveClass('bg-state-accent-active')
  143. })
  144. })
  145. })