Skip to content

Commit

Permalink
MCR-4131: Admin must add an update reason (#2644)
Browse files Browse the repository at this point in the history
* Fix clear dropdown selection

* Add errors and pull out Yup schema

* Cleanup

* Add test for errors.

* Move error up

* Add error summary and test for it.

* Move error up

* Fix combobox error focus.
  • Loading branch information
JasonLin0991 authored Aug 12, 2024
1 parent 291275d commit 5d46323
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 196 deletions.
47 changes: 27 additions & 20 deletions services/app-web/src/pages/LinkYourRates/LinkRateSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export interface LinkRateOptionType {
export type LinkRateSelectPropType = {
name: string
initialValue: string | undefined
alreadySelected?: string[], // used for multi-rate, array of rate IDs helps ensure we can't select rates already selected elsewhere on page
alreadySelected?: string[] // used for multi-rate, array of rate IDs helps ensure we can't select rates already selected elsewhere on page
autofill?: (rateForm: FormikRateForm) => void // used for multi-rates, when called will FieldArray replace the existing form fields with new data
label?: string,
label?: string
stateCode?: string //used to limit rates by state
}

Expand All @@ -51,20 +51,22 @@ export const LinkRateSelect = ({
}: LinkRateSelectPropType & Props<LinkRateOptionType, false>) => {
// const input:IndexRatesInput | undefined ={stateCode}
// TODO figure out why this isn't working - useIndexRatesQuery({variables: { input }})
const { data, loading, error } = useIndexRatesQuery()
const { data, loading, error } = useIndexRatesQuery()

const { getKey } = useS3()
const { logDropdownSelectionEvent } = useTealium()
const [ _field, _meta, helpers] = useField({ name }) // useField only relevant for non-autofill implementations
const [_field, _meta, helpers] = useField({ name }) // useField only relevant for non-autofill implementations

const rates = data?.indexRates.edges.map((e) => e.node) || []

// Sort rates by latest submission in desc order and remove withdrawn
rates.sort(
(a, b) =>
new Date(b.revisions[0].submitInfo?.updatedAt).getTime() -
new Date(a.revisions[0].submitInfo?.updatedAt).getTime()
).filter( (rate)=> rate.withdrawInfo === undefined)
rates
.sort(
(a, b) =>
new Date(b.revisions[0].submitInfo?.updatedAt).getTime() -
new Date(a.revisions[0].submitInfo?.updatedAt).getTime()
)
.filter((rate) => rate.withdrawInfo === undefined)

const rateNames: LinkRateOptionType[] = rates.map((rate) => {
const revision = rate.revisions[0]
Expand Down Expand Up @@ -123,9 +125,11 @@ export const LinkRateSelect = ({
heading: label,
})

if(autofill) {
if (autofill) {
const linkedRateID = newValue.value
const linkedRate = rates.find((rate) => rate.id === linkedRateID)
const linkedRate = rates.find(
(rate) => rate.id === linkedRateID
)
const linkedRateForm: FormikRateForm = convertGQLRateToRateForm(
getKey,
linkedRate
Expand All @@ -136,20 +140,23 @@ export const LinkRateSelect = ({
autofill(linkedRateForm)
} else {
// this path is used for replace/withdraw redundant rates
// we are not autofilling form data, we are just returning the IDs of the rate selectred
await helpers.setValue(
newValue.value)
// we are not autofilling form data, we are just returning the IDs of the rate selected
await helpers.setValue(newValue.value)
}
} else if (action === 'clear') {
logDropdownSelectionEvent({
text: 'clear',
heading: label,
})
if(autofill){
if (autofill) {
const emptyRateForm = convertGQLRateToRateForm(getKey)
// put already selected fields back in place
emptyRateForm.ratePreviouslySubmitted = 'YES'
autofill(emptyRateForm)
} else {
// this path is used for replace/withdraw redundant rates
// we are not autofilling form data, we are just returning the IDs of the rate selected
await helpers.setValue('')
}
}
}
Expand Down Expand Up @@ -183,11 +190,11 @@ export const LinkRateSelect = ({
options={
error || loading
? undefined
: alreadySelected?
rateNames.filter(
(rate) => !alreadySelected.includes(rate.value)
)
: rateNames
: alreadySelected
? rateNames.filter(
(rate) => !alreadySelected.includes(rate.value)
)
: rateNames
}
formatOptionLabel={formatOptionLabel}
isSearchable
Expand Down
148 changes: 122 additions & 26 deletions services/app-web/src/pages/ReplaceRate/ReplaceRate.test.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { screen, waitFor } from '@testing-library/react'
import { renderWithProviders, } from '../../testHelpers'
import { renderWithProviders } from '../../testHelpers'
import {
fetchCurrentUserMock,
mockValidCMSUser,
mockValidAdminUser,
fetchContractMockSuccess,
mockContractPackageSubmitted,
indexRatesMockSuccess
indexRatesMockSuccess,
withdrawAndReplaceRedundantRateMock,
rateDataMock,
} from '../../testHelpers/apolloMocks'
import { RoutesRecord } from '../../constants'
import { Location, Route, Routes } from 'react-router-dom'
import { ReplaceRate } from './ReplaceRate'
import userEvent from '@testing-library/user-event'
import { Rate } from '../../gen/gqlClient'

// Wrap test component in some top level routes to allow getParams to be tested
const wrapInRoutes = (children: React.ReactNode) => {
return (
<Routes>
<Route
path={RoutesRecord.SUBMISSIONS_SUMMARY}
element={<div>Summary page placeholder</div>}
/>
<Route
path={RoutesRecord.SUBMISSIONS_SUMMARY}
element={<div>Summary page placeholder</div>}
/>
<Route path={RoutesRecord.REPLACE_RATE} element={children} />
</Routes>
)
Expand All @@ -31,7 +34,6 @@ describe('ReplaceRate', () => {
vi.resetAllMocks()
})


it('does not render for CMS user', async () => {
const contract = mockContractPackageSubmitted()
renderWithProviders(wrapInRoutes(<ReplaceRate />), {
Expand All @@ -41,19 +43,20 @@ describe('ReplaceRate', () => {
user: mockValidCMSUser(),
statusCode: 200,
}),
fetchContractMockSuccess({ contract})
fetchContractMockSuccess({ contract }),
],
},
routerProvider: {
route: `/submissions/${contract.id}/replace-rate/${contract.packageSubmissions[0].rateRevisions[0].rateID}`,
},
})

const forbidden = await screen.findByText('You do not have permission to view the requested file or resource.')
const forbidden = await screen.findByText(
'You do not have permission to view the requested file or resource.'
)
expect(forbidden).toBeInTheDocument()
})


it('renders without errors for admin user', async () => {
const contract = mockContractPackageSubmitted()
renderWithProviders(wrapInRoutes(<ReplaceRate />), {
Expand All @@ -63,8 +66,8 @@ describe('ReplaceRate', () => {
user: mockValidAdminUser(),
statusCode: 200,
}),
fetchContractMockSuccess({ contract}),
indexRatesMockSuccess()
fetchContractMockSuccess({ contract }),
indexRatesMockSuccess(),
],
},
routerProvider: {
Expand All @@ -74,22 +77,24 @@ describe('ReplaceRate', () => {

await screen.findByRole('form')
expect(
screen.getByRole('heading', { name: 'Replace a rate review' })
screen.getByRole('heading', { name: 'Replace a rate review' })
).toBeInTheDocument()
expect(
screen.getByRole('form', {name: 'Withdraw and replace rate on contract'})
screen.getByRole('form', {
name: 'Withdraw and replace rate on contract',
})
).toBeInTheDocument()
expect(
screen.getByRole('textbox', {name: 'Reason for revoking'})
screen.getByRole('textbox', { name: 'Reason for revoking' })
).toBeInTheDocument()
expect(
screen.getByText('Select a replacement rate')).toBeInTheDocument()
screen.getByText('Select a replacement rate')
).toBeInTheDocument()
expect(
screen.getByRole('button', {name: 'Replace rate'})
screen.getByRole('button', { name: 'Replace rate' })
).toBeInTheDocument()
})


it('cancel button moves admin user back to parent contract summary', async () => {
let testLocation: Location // set up location to track URL change
const contract = mockContractPackageSubmitted()
Expand All @@ -100,22 +105,113 @@ describe('ReplaceRate', () => {
user: mockValidAdminUser(),
statusCode: 200,
}),
fetchContractMockSuccess({ contract}),
indexRatesMockSuccess()
fetchContractMockSuccess({ contract }),
indexRatesMockSuccess(),
],
},
routerProvider: {
route: `/submissions/${contract.id}/replace-rate/${contract.packageSubmissions[0].rateRevisions[0].rateID}`,
},
location: (location) => (testLocation = location),
})
await screen.findByRole('form')
await userEvent.click(screen.getByText('Cancel'))
await waitFor(() => {
expect(testLocation.pathname).toBe(
`/submissions/${contract.id}`
)
await screen.findByRole('form')
await userEvent.click(screen.getByText('Cancel'))
await waitFor(() => {
expect(testLocation.pathname).toBe(`/submissions/${contract.id}`)
})
})

it('shows errors when required fields are not filled in', async () => {
let testLocation: Location // set up location to track URL change
const contract = mockContractPackageSubmitted()
const replacementRates: Rate[] = [
{ ...rateDataMock(), id: 'test-id-123', stateNumber: 3 },
]
const withdrawnRateID =
contract.packageSubmissions[0].rateRevisions[0].rateID
const replaceReason = 'This is a good reason'
renderWithProviders(wrapInRoutes(<ReplaceRate />), {
apolloProvider: {
mocks: [
fetchCurrentUserMock({
user: mockValidAdminUser(),
statusCode: 200,
}),
fetchContractMockSuccess({ contract }),
indexRatesMockSuccess(replacementRates),
withdrawAndReplaceRedundantRateMock({
contract,
input: {
replaceReason,
withdrawnRateID,
contractID: contract.id,
replacementRateID: replacementRates[0].id,
},
}),
],
},
routerProvider: {
route: `/submissions/${contract.id}/replace-rate/${withdrawnRateID}`,
},
location: (location) => (testLocation = location),
})

// Find replace button
const replaceRateButton = await screen.findByRole('button', {
name: 'Replace rate',
})
expect(replaceRateButton).toBeInTheDocument()

// Click replace button to show errors
await userEvent.click(replaceRateButton)

// Check for both errors inline and error summary
expect(
screen.queryAllByText(
'You must provide a reason for revoking this rate certification.'
)
).toHaveLength(2)
expect(
screen.queryAllByText(
'You must select a replacement rate certification.'
)
).toHaveLength(2)

// Fill withdraw reason
const replaceReasonInput = screen.getByRole('textbox', {
name: 'Reason for revoking',
})
expect(replaceReasonInput).toBeInTheDocument()
await userEvent.type(replaceReasonInput, replaceReason)

// Select a replacement rate
const rateDropdown = screen.getByRole('combobox')
expect(rateDropdown).toBeInTheDocument()
await userEvent.click(rateDropdown)

const rateDropdownOptions = screen.getAllByRole('option')
expect(rateDropdownOptions).toHaveLength(1)

await userEvent.click(rateDropdownOptions[0])

// Check errors are gone
expect(
screen.queryAllByText(
'You must provide a reason for revoking this rate certification.'
)
).toHaveLength(0)
expect(
screen.queryAllByText(
'You must select a replacement rate certification.'
)
).toHaveLength(0)

// Click replace rate button
await userEvent.click(replaceRateButton)

// Wait for redirect
await waitFor(() => {
expect(testLocation.pathname).toBe(`/submissions/${contract.id}`)
})
})
})
Loading

0 comments on commit 5d46323

Please sign in to comment.