Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/Connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TokenContext } from '@kyper/tokenprovider'
import { usePrevious } from '@kyper/hooks'

import * as connectActions from 'src/redux/actions/Connect'
import { setWidgetVersion } from 'src/redux/actions/App'
import { addAnalyticPath, removeAnalyticPath } from 'src/redux/reducers/analyticsSlice'

import { isConsentEnabled, loadUserFeatures } from 'src/redux/reducers/userFeaturesSlice'
Expand Down Expand Up @@ -144,6 +145,7 @@ export const Connect: React.FC<ConnectProps> = ({
dispatch(loadProfiles(props.profiles))
dispatch(loadUserFeatures(props.userFeatures))
dispatch(loadExperimentalFeatures(props?.experimentalFeatures || {}))
dispatch(setWidgetVersion(props?.version || null))

// Also important to note that this is a race condition between connect
// mounting and the master data loading the client data. It just so happens
Expand Down
11 changes: 11 additions & 0 deletions src/__tests__/ConnectWidget-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,15 @@ describe('ConnectWidget', () => {
{ timeout: 15000 },
)
}, 35000)

it('stores version metadata in app state from the top-level version prop', async () => {
render(<ConnectWidgetWithoutReduxProvider {...defaultProps} version="abcdef1234567" />, {
apiValue: apiValueMock,
store: activeStore,
})

await waitFor(() => {
expect(activeStore.getState().app.version).toBe('abcdef1234567')
})
})
})
6 changes: 6 additions & 0 deletions src/redux/actions/App.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
export const ActionTypes = {
SESSION_IS_TIMED_OUT: 'app/session_is_timed_out',
HUMAN_EVENT_HAPPENED: 'app/human_event_happened',
SET_WIDGET_VERSION: 'app/set_widget_version',
}

export const setWidgetVersion = (version) => ({
type: ActionTypes.SET_WIDGET_VERSION,
payload: version,
})

export const dispatcher = (dispatch) => ({
markSessionTimedOut: () => dispatch({ type: ActionTypes.SESSION_IS_TIMED_OUT }),
handleHumanEvent: () => dispatch({ type: ActionTypes.HUMAN_EVENT_HAPPENED }),
Expand Down
9 changes: 8 additions & 1 deletion src/redux/actions/__tests__/app-test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dispatcher as appDispatcher, ActionTypes } from 'src/redux/actions/App'
import { dispatcher as appDispatcher, ActionTypes, setWidgetVersion } from 'src/redux/actions/App'
import { createReduxActionUtils } from 'src/utilities/Test'

const { actions, expectDispatch, resetDispatch } = createReduxActionUtils(appDispatcher)
Expand All @@ -12,4 +12,11 @@ describe('app Dispatcher', () => {
actions.markSessionTimedOut()
expectDispatch({ type: ActionTypes.SESSION_IS_TIMED_OUT })
})

it('should create SET_WIDGET_VERSION action', () => {
expect(setWidgetVersion('abc1234')).toEqual({
type: ActionTypes.SET_WIDGET_VERSION,
payload: 'abc1234',
})
})
})
5 changes: 5 additions & 0 deletions src/redux/reducers/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ export const defaultState = {
// credential stuffing. See https://gitlab.mx.com/mx/connect/issues/279
// wether or not we consider a human has used the app.
humanEvent: false,
version: null,
}

const markSessionTimedOut = (state) => ({ ...state, sessionIsTimedOut: true })

const handleHumanEvent = (state) => ({ ...state, humanEvent: true })

const setWidgetVersion = (state, action) => ({ ...state, version: action.payload })

export const app = (state = defaultState, action) => {
switch (action.type) {
case ActionTypes.SESSION_IS_TIMED_OUT:
return markSessionTimedOut(state)
case ActionTypes.HUMAN_EVENT_HAPPENED:
return handleHumanEvent(state)
case ActionTypes.SET_WIDGET_VERSION:
return setWidgetVersion(state, action)
default:
return state
}
Expand Down
8 changes: 7 additions & 1 deletion src/redux/reducers/__tests__/app-test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionTypes } from 'src/redux/actions/App'
import { ActionTypes, setWidgetVersion } from 'src/redux/actions/App'
import { app as reducer, defaultState } from 'src/redux/reducers/App'

const { SESSION_IS_TIMED_OUT } = ActionTypes
Expand All @@ -15,4 +15,10 @@ describe('app reducers', () => {
expect(reducer(undefined, action).sessionIsTimedOut).toBe(true)
})
})

describe('SET_WIDGET_VERSION', () => {
it('should store the widget version', () => {
expect(reducer(undefined, setWidgetVersion('abc1234')).version).toBe('abc1234')
})
})
})
2 changes: 2 additions & 0 deletions src/redux/selectors/app.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const getSessionIsTimedOut = (state) => state.app.sessionIsTimedOut

export const getWidgetVersion = (state) => state.app.version
13 changes: 12 additions & 1 deletion src/views/search/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import { CloseOutline } from '@kyper/icon/CloseOutline'
import { Search as SearchIcon } from '@kyper/icon/Search'
import InputAdornment from '@mui/material/InputAdornment'
import { TextField } from 'src/privacy/input'
import { IconButton } from '@mui/material'
import { IconButton, Snackbar } from '@mui/material'

import { __ } from 'src/utilities/Intl'
import * as connectActions from 'src/redux/actions/Connect'
import { selectConnectConfig } from 'src/redux/reducers/configSlice'
import { getWidgetVersion } from 'src/redux/selectors/app'
import { getMembers } from 'src/redux/selectors/Connect'

import { AnalyticEvents, PageviewInfo } from 'src/const/Analytics'
Expand Down Expand Up @@ -131,6 +132,7 @@ export const Search = React.forwardRef((_, navigationRef) => {
useAnalyticsPath(...PageviewInfo.CONNECT_SEARCH, {}, false)
const [state, dispatch] = useReducer(reducer, initialState)
const [ariaLiveRegionMessage, setAriaLiveRegionMessage] = useState('')
const [headerClicks, setHeaderClicks] = useState(0)
const searchInput = useRef('')
const sendAnalyticsEvent = useAnalyticsEvent()
const postMessageFunctions = useContext(PostMessageContext)
Expand All @@ -140,6 +142,7 @@ export const Search = React.forwardRef((_, navigationRef) => {
// Redux
const reduxDispatch = useDispatch()
const connectConfig = useSelector(selectConnectConfig)
const widgetVersion = useSelector(getWidgetVersion)
const connectedMembers = useSelector(getMembers)
const usePopularOnly = useSelector((state) => {
const clientProfile = state.profiles.clientProfile || {}
Expand Down Expand Up @@ -331,13 +334,21 @@ export const Search = React.forwardRef((_, navigationRef) => {
component={'h2'}
data-test="search-header"
id="connect-search-header"
onClick={() => setHeaderClicks((prev) => prev + 1)}
style={inlineStyles.headerText}
tabIndex={-1}
truncate={false}
variant="H2"
>
{__('Select your institution')}
</Text>
{/* This version is a hidden feature unless a user is told how to find it */}
<Snackbar
autoHideDuration={6000}
message={widgetVersion}
onClose={() => setHeaderClicks(0)}
open={headerClicks >= 5 && Boolean(widgetVersion)}
/>
<TextField
InputProps={{
startAdornment: (
Expand Down
42 changes: 41 additions & 1 deletion src/views/search/__tests__/Search-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React from 'react'
import { render, screen, waitFor, fireEvent } from 'src/utilities/testingLibrary'
import {
render,
screen,
waitFor,
fireEvent,
createTestReduxStore,
} from 'src/utilities/testingLibrary'
import { FAVORITE_INSTITUTIONS, SEARCHED_INSTITUTIONS } from 'src/services/mockedData'
import { Search, buildSearchQuery, getSuggestedInstitutions } from 'src/views/search/Search'
import { VERIFY_MODE, TAX_MODE, AGG_MODE } from 'src/const/Connect'
Expand All @@ -8,6 +14,7 @@ import { __ } from 'src/utilities/Intl'
import { ApiProvider } from 'src/context/ApiContext'
import { apiValue } from 'src/const/apiProviderMock'
import { InstitutionStatusField } from 'src/utilities/institutionStatus'
import { setWidgetVersion } from 'src/redux/actions/App'

describe('Search View', () => {
describe('Search component', () => {
Expand Down Expand Up @@ -77,6 +84,39 @@ describe('Search View', () => {
expect(screen.getByText(__('No results found for ”%1”', searchTerm))).toBeInTheDocument()
})
})

it('shows version after clicking the header five times', async () => {
const ref = React.createRef()
const store = createTestReduxStore()
store.dispatch(setWidgetVersion('abcdef1234567'))

render(<Search {...defaultProps} ref={ref} />, { store })

const header = await screen.findByText('Select your institution')
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

for (let i = 0; i < 5; i++) {
fireEvent.click(header)
}

expect(screen.getByRole('alert')).toHaveTextContent('abcdef1234567')
})

it('does not show version snackbar after five clicks when version is not provided', async () => {
const ref = React.createRef()
const store = createTestReduxStore()

render(<Search {...defaultProps} ref={ref} />, { store })

const header = await screen.findByText('Select your institution')
expect(screen.queryByRole('alert')).not.toBeInTheDocument()

for (let i = 0; i < 5; i++) {
fireEvent.click(header)
}

expect(screen.queryByRole('alert')).not.toBeInTheDocument()
})
})

describe('buildSearchQuery function', () => {
Expand Down
1 change: 1 addition & 0 deletions typings/connectProps.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ interface ConnectProps {
memberPollingMilliseconds?: number
useWebSockets?: boolean
}
version?: string
}
interface ClientConfigType {
_initialValues: string
Expand Down
Loading