Skip to content

Commit

Permalink
(PC-31828)[PRO] fix: prevent user to submit form when non pre-filled …
Browse files Browse the repository at this point in the history
…via EAN searched + vinyl/cd subcat
  • Loading branch information
asaez-pass committed Sep 17, 2024
1 parent e92bdf8 commit fc0eb08
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ type RequiredProps = 'isOfferProductBased'
type DetailsEanSearchTestProps = Pick<DetailsEanSearchProps, RequiredProps>

const EanSearchWrappedWithFormik = ({
isOfferProductBased,
}: DetailsEanSearchTestProps): JSX.Element => {
props: { isOfferProductBased },
subcategoryId = DEFAULT_DETAILS_FORM_VALUES.subcategoryId,
}: {
props: DetailsEanSearchTestProps
subcategoryId?: string
}): JSX.Element => {
const formik = useFormik({
initialValues: DEFAULT_DETAILS_FORM_VALUES,
initialValues: {
...DEFAULT_DETAILS_FORM_VALUES,
subcategoryId,
},
onSubmit: vi.fn(),
})

Expand All @@ -53,10 +60,16 @@ const EanSearchWrappedWithFormik = ({
)
}

const renderDetailsEanSearch = (props: DetailsEanSearchTestProps) => {
const renderDetailsEanSearch = ({
props,
subcategoryId = DEFAULT_DETAILS_FORM_VALUES.subcategoryId,
}: {
props: DetailsEanSearchTestProps
subcategoryId?: string
}) => {
return renderWithProviders(
<IndividualOfferContext.Provider value={contextValue}>
<EanSearchWrappedWithFormik {...props} />
<EanSearchWrappedWithFormik props={props} subcategoryId={subcategoryId} />
</IndividualOfferContext.Provider>,
{
storeOverrides: {
Expand All @@ -78,109 +91,142 @@ const inputLabel = /Scanner ou rechercher un produit par EAN/
const successMessage = /Les informations suivantes ont été synchronisées/
const infoMessage = /Les informations de cette page ne sont pas modifiables/
const errorMessage = /Une erreur est survenue lors de la recherche/
const subCatErrorMessage = /doivent être liées à un produit/
const clearButtonLabel = /Effacer/

describe('DetailsEanSearch', () => {
describe('when an EAN search is performed succesfully', () => {
beforeEach(() => {
vi.spyOn(api, 'getProductByEan').mockResolvedValue({
id: 0,
name: 'Music has the right to children',
description: 'An album by Boards of Canada',
subcategoryId: 'SUPPORT_PHYSIQUE_MUSIQUE_VINYLE',
gtlId: '08000000',
author: 'Boards of Canada',
performer: 'Boards of Canada',
images: {},
describe('before form submission - draft offer has not been created yet', () => {
describe('when no EAN search has been performed', () => {
it('should display a permanent error message if the subcategory requires an EAN', async () => {
renderDetailsEanSearch({
props: { isOfferProductBased: false },
subcategoryId: 'SUPPORT_PHYSIQUE_MUSIQUE_VINYLE',
})

expect(screen.getByText(subCatErrorMessage)).toBeInTheDocument()

const eanInput = screen.getByRole('textbox', { name: inputLabel })
await userEvent.type(eanInput, '9781234567897')

expect(screen.getByText(subCatErrorMessage)).toBeInTheDocument()
})

renderDetailsEanSearch({ isOfferProductBased: false })
it('should let the submit button enabled', async () => {
renderDetailsEanSearch({ props: { isOfferProductBased: false } })

const eanInput = screen.getByRole('textbox', { name: inputLabel })
await userEvent.type(eanInput, '9781234567897')

const button = screen.getByRole('button', { name: buttonLabel })
expect(button).not.toBeDisabled()
})
})

it('should display a success message', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })
describe('when an EAN search is performed succesfully', () => {
beforeEach(() => {
vi.spyOn(api, 'getProductByEan').mockResolvedValue({
id: 0,
name: 'Music has the right to children',
description: 'An album by Boards of Canada',
subcategoryId: 'SUPPORT_PHYSIQUE_MUSIQUE_VINYLE',
gtlId: '08000000',
author: 'Boards of Canada',
performer: 'Boards of Canada',
images: {},
})

renderDetailsEanSearch({ props: { isOfferProductBased: false } })
})

expect(screen.queryByText(successMessage)).not.toBeInTheDocument()
it('should display a success message', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })

await userEvent.type(eanInput, '9781234567897')
await userEvent.click(button)
expect(screen.queryByText(successMessage)).not.toBeInTheDocument()

expect(screen.queryByText(successMessage)).toBeInTheDocument()
})
await userEvent.type(eanInput, '9781234567897')
await userEvent.click(button)

it('should be disabled until the input is cleared', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })
expect(screen.queryByText(successMessage)).toBeInTheDocument()
})

expect(eanInput).not.toBeDisabled()
await userEvent.type(eanInput, '9781234567897')
it('should be disabled until the input is cleared', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })

expect(button).not.toBeDisabled()
await userEvent.click(button)
expect(eanInput).not.toBeDisabled()
await userEvent.type(eanInput, '9781234567897')

expect(eanInput).toBeDisabled()
expect(button).toBeDisabled()
expect(button).not.toBeDisabled()
await userEvent.click(button)

// A clear button appears, when clicked, it should empty
// the input. The search button remains disabled, as
// the input is empty.
const clearButton = screen.getByRole('button', { name: clearButtonLabel })
expect(clearButton).toBeInTheDocument()
await userEvent.click(clearButton)
expect(eanInput).toBeDisabled()
expect(button).toBeDisabled()

const newEanInput = screen.getByRole('textbox', { name: inputLabel })
expect(newEanInput).not.toBeDisabled()
expect(newEanInput).toHaveValue('')
expect(button).toBeDisabled()
})
})
// A clear button appears, when clicked, it should empty
// the input. The search button remains disabled, as
// the input is empty.
const clearButton = screen.getByRole('button', {
name: clearButtonLabel,
})
expect(clearButton).toBeInTheDocument()
await userEvent.click(clearButton)

describe('when an EAN search *was* performed succesfully (after POST)', () => {
beforeEach(() => {
renderDetailsEanSearch({ isOfferProductBased: true })
const newEanInput = screen.getByRole('textbox', { name: inputLabel })
expect(newEanInput).not.toBeDisabled()
expect(newEanInput).toHaveValue('')
expect(button).toBeDisabled()
})
})

it('should not display the clear button anymore', () => {
const clearButton = screen.queryByRole('button', {
name: clearButtonLabel,
describe('when an EAN search ends with an API error (only)', () => {
beforeEach(() => {
vi.spyOn(api, 'getProductByEan').mockRejectedValue(new Error('error'))
renderDetailsEanSearch({ props: { isOfferProductBased: false } })
})
expect(clearButton).not.toBeInTheDocument()
})

it('should display an info message', () => {
expect(screen.queryByText(infoMessage)).toBeInTheDocument()
})
})
it('should display an error message', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })

describe('when an EAN search ends with an error', () => {
beforeEach(() => {
vi.spyOn(api, 'getProductByEan').mockRejectedValue(new Error('error'))
renderDetailsEanSearch({ isOfferProductBased: false })
})
expect(screen.queryByText(errorMessage)).not.toBeInTheDocument()

it('should display an error message', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })
await userEvent.type(eanInput, '9781234567897')
await userEvent.click(button)

expect(screen.queryByText(errorMessage)).not.toBeInTheDocument()
expect(screen.queryByText(errorMessage)).toBeInTheDocument()
})

await userEvent.type(eanInput, '9781234567897')
await userEvent.click(button)
it('should disable the submit button', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })

expect(screen.queryByText(errorMessage)).toBeInTheDocument()
})
await userEvent.type(eanInput, '9781234567897')

expect(button).not.toBeDisabled()
await userEvent.click(button)

it('should disable the submit button', async () => {
const button = screen.getByRole('button', { name: buttonLabel })
const eanInput = screen.getByRole('textbox', { name: inputLabel })
expect(button).toBeDisabled()
})
})
})

await userEvent.type(eanInput, '9781234567897')
describe('after POST request (the form has been submitted)', () => {
describe('when an EAS search was performed succesfully', () => {
beforeEach(() => {
renderDetailsEanSearch({ props: { isOfferProductBased: true } })
})

expect(button).not.toBeDisabled()
await userEvent.click(button)
it('should not display the clear button anymore', () => {
const clearButton = screen.queryByRole('button', {
name: clearButtonLabel,
})
expect(clearButton).not.toBeInTheDocument()
})

expect(button).toBeDisabled()
it('should display an info message', () => {
expect(screen.queryByText(infoMessage)).toBeInTheDocument()
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { TextInput } from 'ui-kit/form/TextInput/TextInput'
import { Tag, TagVariant } from 'ui-kit/Tag/Tag'

import { DetailsFormValues } from '../types'
import { hasMusicType } from '../utils'
import { hasMusicType, isSubCategoryCDOrVinyl } from '../utils'

import styles from './DetailsEanSearch.module.scss'

Expand All @@ -31,13 +31,21 @@ export const DetailsEanSearch = ({
const selectedOffererId = useSelector(selectCurrentOffererId)
const inputRef = useRef<HTMLInputElement>(null)
const [isFetchingProduct, setIsFetchingProduct] = useState(false)
const [subcatError, setSubcatError] = useState<string | null>(null)
const [apiError, setApiError] = useState<string | null>(null)
const [wasCleared, setWasCleared] = useState(false)

const { subCategories } = useIndividualOfferContext()
const { values, errors, setValues, resetForm } =
useFormikContext<DetailsFormValues>()
const { ean, productId } = values
const {
values,
errors: { eanSearch: formikError },
setValues,
resetForm,
} = useFormikContext<DetailsFormValues>()
const { eanSearch: ean, productId, subcategoryId } = values

const isNotAnOfferYetButProductBased = !isOfferProductBased && !!productId
const isProductBased = isOfferProductBased || isNotAnOfferYetButProductBased

useEffect(() => {
setApiError(null)
Expand All @@ -50,6 +58,16 @@ export const DetailsEanSearch = ({
}
}, [wasCleared, productId])

useEffect(() => {
if (!isProductBased && isSubCategoryCDOrVinyl(subcategoryId)) {
setSubcatError(
'Les offres de type CD ou Vinyle doivent être liées à un produit.'
)
} else {
setSubcatError(null)
}
}, [subcategoryId, isProductBased])

const onEanSearch = async () => {
if (ean) {
try {
Expand Down Expand Up @@ -128,13 +146,9 @@ export const DetailsEanSearch = ({
setWasCleared(true)
}

const isNotAnOfferYetButProductBased = !isOfferProductBased && !!productId
const isProductBased = isOfferProductBased || isNotAnOfferYetButProductBased
const hasInputErrored = !!errors.ean

const shouldInputBeDisabled = isProductBased || isFetchingProduct
const shouldButtonBeDisabled =
isProductBased || !ean || hasInputErrored || !!apiError || isFetchingProduct
isProductBased || !ean || !!formikError || !!apiError || isFetchingProduct
const displayClearButton = isNotAnOfferYetButProductBased

const calloutVariant = isNotAnOfferYetButProductBased
Expand All @@ -156,6 +170,10 @@ export const DetailsEanSearch = ({
</>
)

const nonFormikError = subcatError || apiError
const errorArray = [formikError, apiError, subcatError].filter(Boolean)
const externalError = nonFormikError ? errorArray.join('\n') : undefined

return (
<div className={styles['details-ean-search']}>
<div className={styles['details-ean-search-form']}>
Expand All @@ -164,14 +182,14 @@ export const DetailsEanSearch = ({
classNameLabel={styles['details-ean-search-label']}
label={label}
description="Format : EAN à 13 chiffres"
name="ean"
name="eanSearch"
type="text"
disabled={shouldInputBeDisabled}
maxLength={13}
isOptional
countCharacters
{...(apiError && {
externalError: apiError,
{...(externalError && {
externalError,
})}
{...(displayClearButton
? {
Expand Down
8 changes: 2 additions & 6 deletions pro/src/screens/IndividualOffer/DetailsScreen/DetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
CategoryResponseModel,
SubcategoryResponseModel,
VenueListItemResponseModel,
SubcategoryIdEnum,
} from 'apiClient/v1'
import { Callout } from 'components/Callout/Callout'
import { CalloutVariant } from 'components/Callout/types'
Expand All @@ -30,7 +29,7 @@ import { DetailsSubForm } from './DetailsSubForm/DetailsSubForm'
import { Subcategories } from './Subcategories/Subcategories'
import { SuggestedSubcategories } from './SuggestedSubcategories/SuggestedSubcategories'
import { DetailsFormValues } from './types'
import { buildVenueOptions } from './utils'
import { buildVenueOptions, isSubCategoryCDOrVinyl } from './utils'

const DEBOUNCE_TIME_BEFORE_REQUEST = 400

Expand Down Expand Up @@ -129,9 +128,6 @@ export const DetailsForm = ({

const isSubCategorySelected =
subcategoryId !== DEFAULT_DETAILS_FORM_VALUES.subcategoryId
const isSubCategoryCDOrVinyl =
subcategoryId === SubcategoryIdEnum.SUPPORT_PHYSIQUE_MUSIQUE_CD ||
subcategoryId === SubcategoryIdEnum.SUPPORT_PHYSIQUE_MUSIQUE_VINYLE

// Show the field if more than 1 venue (whatever the FF),
// otherwise if there is only 1 venue, we want to show only if both offerAddress and splitOfferEnabled are enabled
Expand Down Expand Up @@ -276,7 +272,7 @@ export const DetailsForm = ({
) : (
<DetailsSubForm
isProductBased={isProductBased}
isOfferCDOrVinyl={isSubCategoryCDOrVinyl}
isOfferCDOrVinyl={isSubCategoryCDOrVinyl(subcategoryId)}
readOnlyFields={readOnlyFields}
onImageUpload={onImageUpload}
onImageDelete={onImageDelete}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@use "styles/mixins/_rem.scss" as rem;

.callout {
margin-top: rem.torem(-32px);
margin-bottom: rem.torem(32px);
}
Loading

0 comments on commit fc0eb08

Please sign in to comment.