Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/modules/navigation/NavLink.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cx from 'classnames'
import PropTypes from 'prop-types'
import React from 'react'
import React, { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'

import { NavLink as UINavLink } from 'cozy-ui/transpiled/react/Nav'
Expand All @@ -19,6 +19,21 @@ const NavLink = ({
clickState: [lastClicked, setLastClicked]
}) => {
const location = useLocation()
const prevPathnameRef = useRef(location.pathname)

useEffect(() => {
const prevPathname = prevPathnameRef.current
prevPathnameRef.current = location.pathname

if (!lastClicked) return

if (navLinkMatch(rx, lastClicked, location.pathname)) {
setLastClicked(null) // route arrived at destination
} else if (location.pathname !== prevPathname) {
setLastClicked(null) // route changed but went elsewhere (e.g. back button)
}
}, [location.pathname, rx, lastClicked, setLastClicked])

const pathname = lastClicked ? lastClicked : location.pathname
const isActive = navLinkMatch(rx, to, pathname)
return (
Expand Down
161 changes: 161 additions & 0 deletions src/modules/navigation/NavLink.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { render, fireEvent, act } from '@testing-library/react'
import React from 'react'
import { useLocation } from 'react-router-dom'

import { NavLink } from './NavLink'

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn()
}))

jest.mock('cozy-ui/transpiled/react/Nav', () => ({
NavLink: { className: 'nav-link', activeClassName: 'active' }
}))

describe('NavLink', () => {
beforeEach(() => {
useLocation.mockReturnValue({ pathname: '/recent' })
})

describe('click handler', () => {
it('calls setLastClicked with to on click', () => {
const setLastClicked = jest.fn()
const { getByRole } = render(
<NavLink to="/folder" clickState={[null, setLastClicked]}>
Drive
</NavLink>
)
fireEvent.click(getByRole('link'))
expect(setLastClicked).toHaveBeenCalledWith('/folder')
})

it('prevents default when no to prop', () => {
const setLastClicked = jest.fn()
const { getByRole } = render(
<NavLink clickState={[null, setLastClicked]}>Drive</NavLink>
)
const event = new MouseEvent('click', { bubbles: true, cancelable: true })
getByRole('link').dispatchEvent(event)
expect(event.defaultPrevented).toBe(true)
})
})

describe('optimistic active state', () => {
it('shows as active when lastClicked matches rx', () => {
const rx = /\/(folder|nextcloud)(\/.*)?/
const { getByRole } = render(
<NavLink to="/folder" rx={rx} clickState={['/folder', jest.fn()]}>
Drive
</NavLink>
)
expect(getByRole('link').className).toContain('active')
})

it('is not active when lastClicked does not match rx', () => {
const rx = /\/(folder|nextcloud)(\/.*)?/
const { getByRole } = render(
<NavLink to="/folder" rx={rx} clickState={['/recent', jest.fn()]}>
Drive
</NavLink>
)
expect(getByRole('link').className).not.toContain('active')
})
})

describe('useEffect: reset lastClicked when route arrives', () => {
it('does not reset lastClicked while the route has not yet changed', () => {
const setLastClicked = jest.fn()
useLocation.mockReturnValue({ pathname: '/recent' })

render(
<NavLink
to="/folder"
rx={/\/(folder|nextcloud)(\/.*)?/}
clickState={['/folder', setLastClicked]}
>
Drive
</NavLink>
)

expect(setLastClicked).not.toHaveBeenCalledWith(null)
})

it('resets lastClicked once the route matches rx', async () => {
const setLastClicked = jest.fn()
useLocation.mockReturnValue({ pathname: '/recent' })
const rx = /\/(folder|nextcloud)(\/.*)?/

const { rerender } = render(
<NavLink to="/folder" rx={rx} clickState={['/folder', setLastClicked]}>
Drive
</NavLink>
)

useLocation.mockReturnValue({ pathname: '/folder' })
await act(async () => {
rerender(
<NavLink
to="/folder"
rx={rx}
clickState={['/folder', setLastClicked]}
>
Drive
</NavLink>
)
})

expect(setLastClicked).toHaveBeenCalledWith(null)
})
Comment on lines +84 to +109
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: 'resets lastClicked once the route matches rx','resets lastClicked once the route matches without rx','resets lastClicked when route changes to a different destination (e.g. back button)'

Suppress


it('resets lastClicked once the route matches without rx', async () => {
const setLastClicked = jest.fn()
useLocation.mockReturnValue({ pathname: '/recent' })

const { rerender } = render(
<NavLink to="folder" clickState={['folder', setLastClicked]}>
Drive
</NavLink>
)

useLocation.mockReturnValue({ pathname: '/folder' })
await act(async () => {
rerender(
<NavLink to="folder" clickState={['folder', setLastClicked]}>
Drive
</NavLink>
)
})

expect(setLastClicked).toHaveBeenCalledWith(null)
})

it('resets lastClicked when route changes to a different destination (e.g. back button)', async () => {
const setLastClicked = jest.fn()
useLocation.mockReturnValue({ pathname: '/recent' })
const rx = /\/(folder|nextcloud)(\/.*)?/

const { rerender } = render(
<NavLink to="/folder" rx={rx} clickState={['/folder', setLastClicked]}>
Drive
</NavLink>
)

// Route changed but to /trash, not /folder (e.g. back button or unexpected navigation)
useLocation.mockReturnValue({ pathname: '/trash' })
await act(async () => {
rerender(
<NavLink
to="/folder"
rx={rx}
clickState={['/folder', setLastClicked]}
>
Drive
</NavLink>
)
})

expect(setLastClicked).toHaveBeenCalledWith(null)
})
})
})
Loading