Skip to content

Commit

Permalink
Only show tour button on some pages (#5816)
Browse files Browse the repository at this point in the history
* Only show tour button on some pages

* Exclude tour button for domains page

* Fix TourComponent.test.js

* Remove mocked handleJoyrideCallback testing

* Remove unused import

* Fix TourButton.test.js
  • Loading branch information
FestiveKyle authored Oct 17, 2024
1 parent 6503568 commit 3a86771
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 112 deletions.
2 changes: 1 addition & 1 deletion frontend/src/landing/LandingPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function LandingPage({ loginRequired, isLoggedIn }) {

return (
<Stack w="100%">
<TourComponent page="landingPage" />
<TourComponent />
<Box mb="16" textAlign="left" px="4">
<Heading as="h1">
<Trans>Track Digital Security</Trans>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/userOnboarding/__tests__/TourButton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('TourTextButton', () => {
const mockStartTour = jest.fn()
useTourModule.useTour.mockReturnValue({ startTour: mockStartTour })
const mockUseLocation = require('react-router-dom').useLocation
mockUseLocation.mockReturnValue({ pathname: '/organizations' })
mockUseLocation.mockReturnValue({ pathname: '/' })

const { getByRole } = render(
<MemoryRouter>
Expand All @@ -45,6 +45,6 @@ describe('TourTextButton', () => {
//Simulate button click
fireEvent.click(getByRole('button', { name: /Start Tour/i }))

expect(mockStartTour).toHaveBeenCalledWith('organizationsPage')
expect(mockStartTour).toHaveBeenCalledWith('landingPage')
})
})
180 changes: 92 additions & 88 deletions frontend/src/userOnboarding/__tests__/TourComponent.test.js
Original file line number Diff line number Diff line change
@@ -1,120 +1,124 @@
import React from 'react'
import { render, screen } from '@testing-library/react'
import { render, waitFor } from '@testing-library/react'
import { TourComponent } from '../components/TourComponent'
import { useTour } from '../hooks/useTour'

jest.mock('../hooks/useTour')
import { MockedProvider } from '@apollo/client/testing'
import { UserVarProvider } from '../../utilities/userState'
import { makeVar } from '@apollo/client'
import { I18nProvider } from '@lingui/react'
import { ChakraProvider, theme } from '@chakra-ui/react'
import { MemoryRouter } from 'react-router-dom'
import { TourProvider } from '../contexts/TourContext'
import { setupI18n } from '@lingui/core'
import { en } from 'make-plural'
import { fireEvent } from '@testing-library/dom'

const i18n = setupI18n({
locale: 'en',
messages: {
en: {},
},
localeData: {
en: { plurals: en },
},
})

jest.mock('../config/tourSteps', () => ({
mainTourSteps: {
home: {
landingPage: {
requiresAuth: false,
steps: [
{
target: '.step-1',
content: 'Home Step 1',
content: 'Landing Step 1',
disableBeacon: true,
},
{
target: '.step-2',
content: 'Home Step 2',
content: 'Landing Step 2',
disableBeacon: true,
},
],
},
},
}))

// Mock the Joyride component
jest.mock('react-joyride', () => {
const tourSteps = require('../config/tourSteps').mainTourSteps['home']['steps']
const MockJoyride = () => <div data-testid="mockJoyride">Mocked Joyride with steps: {JSON.stringify(tourSteps)}</div>
MockJoyride.displayName = 'MockJoyride'
return MockJoyride
})

const mockLocalStorage = (() => {
let store = {}
return {
getItem(key) {
return store[key] || null
},
setItem(key, value) {
store[key] = value.toString()
},
removeItem(key) {
delete store[key]
},
clear() {
store = {}
},
}
})()

describe('TourComponent', () => {
beforeEach(() => {
useTour.mockReturnValue({
isTourOpen: false,
startTour: jest.fn(),
endTour: jest.fn(),
})
mockLocalStorage.clear()
// Replace the real localStorage with our mock
Object.defineProperty(window, 'localStorage', { value: mockLocalStorage })
})

afterEach(() => {
jest.clearAllMocks()
localStorage.clear()
})

test('TourComponent renders with mock Joyride', () => {
render(<TourComponent page="home" />)
expect(screen.getByTestId('mockJoyride')).toBeInTheDocument()
})
it('renders the mocked landing page tour for users who have not seen it', async () => {
expect(localStorage.getItem('hasSeenTour_landingPage')).toBe(null)

const { getByText, getByRole, queryByText } = render(
<MockedProvider mocks={[]} addTypename={false}>
<UserVarProvider userVar={makeVar({ jwt: null, tfaSendMethod: null, userName: null })}>
<I18nProvider i18n={i18n}>
<ChakraProvider theme={theme}>
<MemoryRouter initialEntries={['']} initialIndex={0}>
<TourProvider>
<TourComponent />
<p className="step-1">Test 1</p>
<p className="step-2">Test 2</p>
</TourProvider>
</MemoryRouter>
</ChakraProvider>
</I18nProvider>
</UserVarProvider>
</MockedProvider>,
)

// Element will exist but not be visible
expect(queryByText(/Landing Step 1/)).not.toBeNull()

await waitFor(() => {
expect(getByText(/Landing Step 1/)).toBeVisible()
})

it('does not start the tour for users who have seen it', () => {
localStorage.setItem('hasSeenTour_home', 'true')
const mockStartTour = jest.fn()
useTour.mockReturnValue({ isTourOpen: false, startTour: mockStartTour, endTour: jest.fn() })
let nextBtn = getByRole('button', { name: /Next/ })
expect(nextBtn).toBeInTheDocument()
fireEvent.click(nextBtn)

render(<TourComponent page="home" />)
await waitFor(() => {
expect(getByText(/Landing Step 2/)).toBeVisible()
})

expect(mockStartTour).not.toHaveBeenCalled()
})
})
nextBtn = getByRole('button', { name: /Finish/ })
expect(nextBtn).toBeInTheDocument()
fireEvent.click(nextBtn)

describe('handleJoyrideCallback', () => {
const endTourMock = jest.fn()
const originalSetItem = localStorage.setItem

function createHandleJoyrideCallbackFunction() {
return function handleJoyrideCallback({ status }) {
if (status === 'finished' || status === 'skipped') {
localStorage.setItem(`hasSeenTour_home`, 'true')
endTourMock()
}
}
}

beforeEach(() => {
localStorage.setItem = jest.fn()
endTourMock.mockClear()
useTour.mockReturnValue({
isTourOpen: false,
startTour: jest.fn(),
endTour: jest.fn(),
await waitFor(() => {
expect(queryByText(/Landing Step 2/)).toBeNull()
})
})

afterEach(() => {
localStorage.setItem = originalSetItem
// Ensure that the tour has been marked as seen
expect(localStorage.getItem('hasSeenTour_landingPage')).toBe('true')
})

it.each(['finished', 'skipped'])('sets localStorage and ends tour when status is %s', (status) => {
const page = 'home'
const handleJoyrideCallback = createHandleJoyrideCallbackFunction({ endTour: jest.fn(), page })

handleJoyrideCallback({ status, type: 'any', action: 'any' })

expect(localStorage.setItem).toHaveBeenCalledWith(`hasSeenTour_${page}`, 'true')
expect(endTourMock).toHaveBeenCalled()
it('does not render the tour for users who have seen it', async () => {
localStorage.setItem('hasSeenTour_landingPage', 'true')

expect(localStorage.getItem('hasSeenTour_landingPage')).toBe('true')

const { queryByText } = render(
<MockedProvider mocks={[]} addTypename={false}>
<UserVarProvider userVar={makeVar({ jwt: null, tfaSendMethod: null, userName: null })}>
<I18nProvider i18n={i18n}>
<ChakraProvider theme={theme}>
<MemoryRouter initialEntries={['']} initialIndex={0}>
<TourProvider>
<TourComponent />
<p className="step-1">Test 1</p>
<p className="step-2">Test 2</p>
</TourProvider>
</MemoryRouter>
</ChakraProvider>
</I18nProvider>
</UserVarProvider>
</MockedProvider>,
)

// Element should not exist
expect(queryByText(/Landing Step 1/)).toBeNull()
})
})
22 changes: 14 additions & 8 deletions frontend/src/userOnboarding/components/TourButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import { useTour } from '../hooks/useTour'
import { Button } from '@chakra-ui/react'
import { Trans } from '@lingui/macro'

const toursConfig = {
//list of pages with their paths
export const toursConfig = {
// list of pages with their paths
// Start tour button will only appear on these pages
'/': 'landingPage',
'/organizations': 'organizationsPage',
'/domains': 'domainPage',
'/my-tracker/summary': ' myTrackerPage',
'/dmarc-summaries': 'dmarcSummariesPage',
'/admin/organizations': 'adminProfilePage',
// '/organizations': 'organizationsPage',
// '/domains': 'domainPage',
// '/my-tracker/summary': ' myTrackerPage',
// '/dmarc-summaries': 'dmarcSummariesPage',
// '/admin/organizations': 'adminProfilePage',
}
//Tour button as an icon, made for the individual pages (not needed for top banner)

Expand Down Expand Up @@ -43,11 +44,16 @@ export const TourButton = () => {
const { pathname } = useLocation()
const { startTour } = useTour()

const tourName = toursConfig[pathname]

const handleStartTour = () => {
const tourName = toursConfig[pathname]
if (tourName) startTour(tourName)
}

if (!tourName) {
return null
}

return (
<Button onClick={handleStartTour} variant="primaryWhite" mx="2" display={{ base: 'none', md: 'inline' }}>
<Trans>Start Tour</Trans>
Expand Down
33 changes: 20 additions & 13 deletions frontend/src/userOnboarding/components/TourComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,48 @@ import { mainTourSteps } from '../config/tourSteps'
import { Trans } from '@lingui/macro'
import { useUserVar } from '../../utilities/userState'
import theme from '../../theme/canada'
import PropTypes from 'prop-types'
import { useLocation } from 'react-router-dom'
import { toursConfig } from './TourButton'

export const TourComponent = ({ page }) => {
export const TourComponent = () => {
const { isEmailValidated } = useUserVar()
const { isTourOpen, endTour, startTour } = useTour()
const [tourKey, setTourKey] = useState(0)
const { darkOrange } = theme.colors.tracker.logo

//handles starting the tour based on the page and user state
const { pathname } = useLocation()

const tourName = toursConfig[pathname]

// handles starting the tour based on the page and user state
useEffect(() => {
const hasSeenTour = localStorage.getItem(`hasSeenTour_${page}`)
if (!tourName) return

const hasSeenTour = localStorage.getItem(`hasSeenTour_${tourName}`)
if (
!hasSeenTour &&
(!mainTourSteps[page]['requiresAuth'] || (mainTourSteps[page]['requiresAuth'] && isEmailValidated()))
(!mainTourSteps[tourName]['requiresAuth'] || (mainTourSteps[tourName]['requiresAuth'] && isEmailValidated()))
)
startTour()
}, [page, startTour])
}, [tourName, startTour])

useEffect(() => {
if (isTourOpen) setTourKey((prev) => prev + 1)
}, [isTourOpen])

if (!tourName) {
return null
}

// handles the finishing and skipping/closing of tour
const handleJoyrideCallback = ({ status, type, action }) => {
if (['finished', 'skipped'].includes(status)) {
localStorage.setItem(`hasSeenTour_${page}`, true)
localStorage.setItem(`hasSeenTour_${tourName}`, true)
endTour()
}

if (type === 'step:after' && action === 'close') {
localStorage.setItem(`hasSeenTour_${page}`, true)
localStorage.setItem(`hasSeenTour_${tourName}`, true)
endTour()
}
}
Expand All @@ -44,7 +55,7 @@ export const TourComponent = ({ page }) => {
return (
<Joyride
key={tourKey}
steps={mainTourSteps[page]['steps']}
steps={mainTourSteps[tourName]['steps']}
run={isTourOpen}
continuous={true}
showProgress={false}
Expand All @@ -69,7 +80,3 @@ export const TourComponent = ({ page }) => {
/>
)
}

TourComponent.propTypes = {
page: PropTypes.string.isRequired,
}

0 comments on commit 3a86771

Please sign in to comment.