mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Broaden full-field validation coverage
This commit is contained in:
@@ -18,7 +18,7 @@ import { ProjectTab } from './Tabs/ProjectTab'
|
||||
import { SpeciesTab } from './Tabs/SpeciesTab'
|
||||
import { TaphonomyTab } from './Tabs/TaphonomyTab'
|
||||
import { EditDataType, LocalityDetailsType, ValidationErrors } from '@/shared/types'
|
||||
import { validateLocality } from '@/shared/validators/locality'
|
||||
import { validateLocality, validateLocalityFields } from '@/shared/validators/locality'
|
||||
import { UpdateTab } from '../DetailView/common/UpdateTab'
|
||||
import { emptyLocality } from '../DetailView/common/defaultValues'
|
||||
import { useNotify } from '@/hooks/notification'
|
||||
@@ -164,6 +164,7 @@ export const LocalityDetails = ({
|
||||
isNew={isNew}
|
||||
onWrite={onWrite}
|
||||
validator={validateLocality}
|
||||
validateFields={validateLocalityFields}
|
||||
deleteFunction={deleteFunction}
|
||||
hasStagingMode
|
||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEditMuseumMutation, useGetMuseumDetailsQuery } from '../../redux/mus
|
||||
import { DetailView, TabType } from '../DetailView/DetailView'
|
||||
import { MuseumInfoTab } from './Tabs/MuseumInfoTab'
|
||||
import { emptyMuseum } from '../DetailView/common/defaultValues'
|
||||
import { validateMuseum } from '@/shared/validators/museum'
|
||||
import { validateMuseum, validateMuseumFields } from '@/shared/validators/museum'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { CircularProgress } from '@mui/material'
|
||||
import { Museum, EditDataType, ValidationErrors } from '@/shared/types'
|
||||
@@ -58,6 +58,7 @@ export const MuseumDetails = ({
|
||||
onWrite={onWrite}
|
||||
isNew={isNew}
|
||||
validator={validateMuseum}
|
||||
validateFields={validateMuseumFields}
|
||||
deleteFunction={undefined}
|
||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PersonTab } from './Tabs/PersonTab'
|
||||
import { PersonRelationsTab } from './Tabs/PersonRelationsTab'
|
||||
import { useUser } from '@/hooks/user'
|
||||
import { EditDataType, PersonDetailsType, Role, ValidationErrors } from '@/shared/types'
|
||||
import { validatePerson } from '@/shared/validators/person'
|
||||
import { validatePerson, validatePersonFields } from '@/shared/validators/person'
|
||||
import { useNotify } from '@/hooks/notification'
|
||||
import { useEffect } from 'react'
|
||||
import { emptyPerson } from '../DetailView/common/defaultValues'
|
||||
@@ -127,6 +127,7 @@ export const PersonDetails = () => {
|
||||
tabs={tabs}
|
||||
data={isNew ? emptyPerson : data!}
|
||||
validator={validatePerson}
|
||||
validateFields={validatePersonFields}
|
||||
deleteFunction={deleteFunction}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DetailView, TabType } from '../DetailView/DetailView'
|
||||
import { CoordinatorTab } from './Tabs/CoordinatorTab'
|
||||
import { EditDataType, RegionDetails as RegionDetailsType, ValidationErrors } from '@/shared/types'
|
||||
import { useNotify } from '@/hooks/notification'
|
||||
import { validateRegion } from '@/shared/validators/region'
|
||||
import { validateRegion, validateRegionFields } from '@/shared/validators/region'
|
||||
import { useEffect } from 'react'
|
||||
import { emptyRegion } from '../DetailView/common/defaultValues'
|
||||
|
||||
@@ -66,6 +66,7 @@ export const RegionDetails = () => {
|
||||
data={isNew ? emptyRegion : data!}
|
||||
onWrite={onWrite}
|
||||
validator={validateRegion}
|
||||
validateFields={validateRegionFields}
|
||||
deleteFunction={deleteFunction}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ import { TaxonomyTab } from './Tabs/TaxonomyTab'
|
||||
import { TeethTab } from './Tabs/TeethTab'
|
||||
import { UpdateTab } from '../DetailView/common/UpdateTab'
|
||||
import { EditDataType, SpeciesDetailsType, ValidationErrors } from '@/shared/types'
|
||||
import { validateSpecies } from '@/shared/validators/species'
|
||||
import { validateSpecies, validateSpeciesFields } from '@/shared/validators/species'
|
||||
import { emptySpecies } from '../DetailView/common/defaultValues'
|
||||
import { useNotify } from '@/hooks/notification'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -124,6 +124,7 @@ export const SpeciesDetails = ({
|
||||
taxonomy={true}
|
||||
hasStagingMode
|
||||
validator={validateSpecies}
|
||||
validateFields={validateSpeciesFields}
|
||||
deleteFunction={deleteFunction}
|
||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { UpdateTab } from '../DetailView/common/UpdateTab'
|
||||
import { DetailView, TabType } from '../DetailView/DetailView'
|
||||
import { BoundTab } from './Tabs/BoundTab'
|
||||
import { TimeUnitTab } from './Tabs/TimeUnitTab.tsx'
|
||||
import { validateTimeBound } from '@/shared/validators/timeBound'
|
||||
import { validateTimeBound, validateTimeBoundFields } from '@/shared/validators/timeBound'
|
||||
|
||||
export const TimeBoundDetails = ({
|
||||
wrapWithUnsavedChangesProvider = true,
|
||||
@@ -108,6 +108,7 @@ export const TimeBoundDetails = ({
|
||||
isNew={isNew}
|
||||
onWrite={onWrite}
|
||||
validator={validateTimeBound}
|
||||
validateFields={validateTimeBoundFields}
|
||||
deleteFunction={deleteFunction}
|
||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { DetailView, TabType } from '../DetailView/DetailView'
|
||||
import { LocalityTab } from './Tabs/LocalityTab'
|
||||
import { TimeUnitTab } from './Tabs/TimeUnitTab'
|
||||
import { TimeUnitUpdateTab } from './Tabs/TimeUnitUpdateTab'
|
||||
import { validateTimeUnit } from '@/shared/validators/timeUnit'
|
||||
import { validateTimeUnit, validateTimeUnitFields } from '@/shared/validators/timeUnit'
|
||||
import { makeEditData } from '../DetailView/Context/DetailContext'
|
||||
import { useDeleteTimeUnit } from '@/hooks/useDeleteTimeUnit'
|
||||
import { getApiErrorMessage, isDuplicateNameError } from '@/utils/api'
|
||||
@@ -112,6 +112,7 @@ export const TimeUnitDetails = ({
|
||||
data={isNew ? emptyTimeUnit : data!}
|
||||
onWrite={onWrite}
|
||||
validator={validateTimeUnit}
|
||||
validateFields={validateTimeUnitFields}
|
||||
deleteFunction={deleteFunction}
|
||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EditDataType, LocalityDetailsType } from '../types'
|
||||
import { Validators, validator } from './validator'
|
||||
import { Validators, validateFields, validator } from './validator'
|
||||
import { validCountries } from './countryList'
|
||||
|
||||
const pollenFields = ['pers_pollen_ap', 'pers_pollen_nap', 'pers_pollen_other'] as const
|
||||
@@ -58,10 +58,9 @@ const validatePollenRecordTotal = (editData: Partial<EditDataType<LocalityDetail
|
||||
return
|
||||
}
|
||||
|
||||
export const validateLocality = (
|
||||
editData: EditDataType<LocalityDetailsType>,
|
||||
fieldName: keyof EditDataType<LocalityDetailsType>
|
||||
) => {
|
||||
const createLocalityValidators = (
|
||||
editData: EditDataType<LocalityDetailsType>
|
||||
): Validators<Partial<EditDataType<LocalityDetailsType>>> => {
|
||||
const isAbsoluteDatingMethod = editData.date_meth === 'absolute'
|
||||
const isTimeUnitDatingMethod = editData.date_meth === 'time_unit'
|
||||
const isCompositeDatingMethod = editData.date_meth === 'composite'
|
||||
@@ -80,7 +79,7 @@ export const validateLocality = (
|
||||
|
||||
const compositeDatingMethodRequiredText = 'One age row must follow the rules for Absolute, the other for Time Unit'
|
||||
|
||||
const validators: Validators<Partial<EditDataType<LocalityDetailsType>>> = {
|
||||
return {
|
||||
// const isNew = editData.lid === undefined
|
||||
date_meth: {
|
||||
name: 'Dating method',
|
||||
@@ -266,6 +265,18 @@ export const validateLocality = (
|
||||
asString: (value: string) => validateFraction('Maximum fraction', value),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const validateLocality = (
|
||||
editData: EditDataType<LocalityDetailsType>,
|
||||
fieldName: keyof EditDataType<LocalityDetailsType>
|
||||
) => {
|
||||
const validators = createLocalityValidators(editData)
|
||||
return validator<EditDataType<LocalityDetailsType>>(validators, editData, fieldName)
|
||||
}
|
||||
|
||||
export const validateLocalityFields = (editData: Partial<EditDataType<LocalityDetailsType>>) =>
|
||||
validateFields<EditDataType<LocalityDetailsType>>(
|
||||
createLocalityValidators(editData as EditDataType<LocalityDetailsType>),
|
||||
editData
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EditDataType, Museum } from '../types'
|
||||
import { Validators, validator } from './validator'
|
||||
import { Validators, validateFields, validator } from './validator'
|
||||
|
||||
/*
|
||||
museum: string;
|
||||
@@ -14,36 +14,39 @@ import { Validators, validator } from './validator'
|
||||
used_gene: boolean | null;
|
||||
*/
|
||||
|
||||
export const validateMuseum = (editData: EditDataType<Museum>, fieldName: keyof EditDataType<Museum>) => {
|
||||
const validators: Validators<Partial<EditDataType<Museum>>> = {
|
||||
museum: {
|
||||
name: 'Museum',
|
||||
required: true,
|
||||
asString: museumCode => {
|
||||
if (museumCode.indexOf(' ') !== -1) return 'Museum code must not contain a space'
|
||||
return null
|
||||
},
|
||||
const museumValidators: Validators<Partial<EditDataType<Museum>>> = {
|
||||
museum: {
|
||||
name: 'Museum',
|
||||
required: true,
|
||||
asString: museumCode => {
|
||||
if (museumCode.indexOf(' ') !== -1) return 'Museum code must not contain a space'
|
||||
return null
|
||||
},
|
||||
institution: {
|
||||
name: 'Institution',
|
||||
required: true,
|
||||
},
|
||||
institution: {
|
||||
name: 'Institution',
|
||||
required: true,
|
||||
},
|
||||
city: {
|
||||
name: 'City',
|
||||
required: true,
|
||||
},
|
||||
country: {
|
||||
name: 'Country',
|
||||
required: true,
|
||||
},
|
||||
state_code: {
|
||||
name: 'State code',
|
||||
asString: value => {
|
||||
if (value.length > 5) return 'State code must contain a maximum of 5 characters'
|
||||
return null
|
||||
},
|
||||
city: {
|
||||
name: 'City',
|
||||
required: true,
|
||||
},
|
||||
country: {
|
||||
name: 'Country',
|
||||
required: true,
|
||||
},
|
||||
state_code: {
|
||||
name: 'State code',
|
||||
asString: value => {
|
||||
if (value.length > 5) return 'State code must contain a maximum of 5 characters'
|
||||
return null
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return validator<EditDataType<Museum>>(validators, editData, fieldName)
|
||||
},
|
||||
}
|
||||
|
||||
export const validateMuseum = (editData: EditDataType<Museum>, fieldName: keyof EditDataType<Museum>) => {
|
||||
return validator<EditDataType<Museum>>(museumValidators, editData, fieldName)
|
||||
}
|
||||
|
||||
export const validateMuseumFields = (editData: Partial<EditDataType<Museum>>) =>
|
||||
validateFields<EditDataType<Museum>>(museumValidators, editData)
|
||||
|
||||
@@ -1,51 +1,54 @@
|
||||
import { EditDataType, PersonDetailsType } from '../types'
|
||||
import { Validators, validator } from './validator'
|
||||
import { Validators, validateFields, validator } from './validator'
|
||||
|
||||
const personValidators: Validators<Partial<EditDataType<PersonDetailsType>>> = {
|
||||
initials: {
|
||||
name: 'initials',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 8,
|
||||
},
|
||||
first_name: {
|
||||
name: 'First Name',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 20,
|
||||
},
|
||||
surname: {
|
||||
name: 'Surname',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 20,
|
||||
},
|
||||
email: {
|
||||
name: 'Email',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 5,
|
||||
maxLength: 30,
|
||||
},
|
||||
organization: {
|
||||
name: 'Organization',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 30,
|
||||
},
|
||||
country: {
|
||||
name: 'Country',
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const validatePerson = (
|
||||
editData: EditDataType<PersonDetailsType>,
|
||||
fieldName: keyof EditDataType<PersonDetailsType>
|
||||
) => {
|
||||
const validators: Validators<Partial<EditDataType<PersonDetailsType>>> = {
|
||||
initials: {
|
||||
name: 'initials',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 8,
|
||||
},
|
||||
first_name: {
|
||||
name: 'First Name',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 20,
|
||||
},
|
||||
surname: {
|
||||
name: 'Surname',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 20,
|
||||
},
|
||||
email: {
|
||||
name: 'Email',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 5,
|
||||
maxLength: 30,
|
||||
},
|
||||
organization: {
|
||||
name: 'Organization',
|
||||
required: true,
|
||||
asString: true,
|
||||
minLength: 2,
|
||||
maxLength: 30,
|
||||
},
|
||||
country: {
|
||||
name: 'Country',
|
||||
required: true,
|
||||
},
|
||||
}
|
||||
|
||||
return validator<EditDataType<PersonDetailsType>>(validators, editData, fieldName)
|
||||
return validator<EditDataType<PersonDetailsType>>(personValidators, editData, fieldName)
|
||||
}
|
||||
|
||||
export const validatePersonFields = (editData: Partial<EditDataType<PersonDetailsType>>) =>
|
||||
validateFields<EditDataType<PersonDetailsType>>(personValidators, editData)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EditDataType, RegionCoordinator, RegionCountry, RegionDetails } from '../types'
|
||||
import { Validators, validator, ValidationError } from './validator'
|
||||
import { Validators, validateFields, validator, ValidationError } from './validator'
|
||||
|
||||
const countryCheck = (countries: EditDataType<RegionCountry[]>) => {
|
||||
for (const country of countries) {
|
||||
@@ -21,29 +21,32 @@ const coordinatorCheck = (coordinators: EditDataType<RegionCoordinator[]>) => {
|
||||
}
|
||||
return null as ValidationError
|
||||
}
|
||||
export const validateRegion = (editData: EditDataType<RegionDetails>, fieldName: keyof EditDataType<RegionDetails>) => {
|
||||
const validators: Validators<Partial<EditDataType<RegionDetails>>> = {
|
||||
reg_coord_id: {
|
||||
name: 'reg_coord_id',
|
||||
required: true,
|
||||
const regionValidators: Validators<Partial<EditDataType<RegionDetails>>> = {
|
||||
reg_coord_id: {
|
||||
name: 'reg_coord_id',
|
||||
required: true,
|
||||
},
|
||||
region: {
|
||||
name: 'Region',
|
||||
required: true,
|
||||
asString: value => {
|
||||
if (value.trim().length === 0) return 'Region name must not be empty'
|
||||
return null
|
||||
},
|
||||
region: {
|
||||
name: 'Region',
|
||||
required: true,
|
||||
asString: value => {
|
||||
if (value.trim().length === 0) return 'Region name must not be empty'
|
||||
return null
|
||||
},
|
||||
},
|
||||
now_reg_coord_country: {
|
||||
name: 'Countries',
|
||||
miscArray: countryCheck,
|
||||
},
|
||||
now_reg_coord_people: {
|
||||
name: 'Region Coordinators',
|
||||
miscArray: coordinatorCheck,
|
||||
},
|
||||
}
|
||||
|
||||
return validator<EditDataType<RegionDetails>>(validators, editData, fieldName)
|
||||
},
|
||||
now_reg_coord_country: {
|
||||
name: 'Countries',
|
||||
miscArray: countryCheck,
|
||||
},
|
||||
now_reg_coord_people: {
|
||||
name: 'Region Coordinators',
|
||||
miscArray: coordinatorCheck,
|
||||
},
|
||||
}
|
||||
|
||||
export const validateRegion = (editData: EditDataType<RegionDetails>, fieldName: keyof EditDataType<RegionDetails>) => {
|
||||
return validator<EditDataType<RegionDetails>>(regionValidators, editData, fieldName)
|
||||
}
|
||||
|
||||
export const validateRegionFields = (editData: Partial<EditDataType<RegionDetails>>) =>
|
||||
validateFields<EditDataType<RegionDetails>>(regionValidators, editData)
|
||||
|
||||
@@ -1,194 +1,201 @@
|
||||
import { taxonStatusOptions } from '../taxonStatusOptions'
|
||||
import { EditDataType, SpeciesDetailsType } from '../types'
|
||||
import { Validators, validator } from './validator'
|
||||
import { Validators, validateFields, validator } from './validator'
|
||||
|
||||
const isEmptyUniqIdentifier = (unique_identifier: string) => {
|
||||
if (unique_identifier === '' || unique_identifier === '-') return true
|
||||
return false
|
||||
}
|
||||
|
||||
const validatePositiveInteger = (name: string, value: number) => {
|
||||
if (!Number.isInteger(value) || value < 1) return `${name} must be a positive integer.`
|
||||
return
|
||||
}
|
||||
|
||||
const validateNonNegativeInteger = (name: string, value: number) => {
|
||||
if (!Number.isInteger(value) || value < 0) return `${name} must be a non-negative integer.`
|
||||
return
|
||||
}
|
||||
|
||||
const validateIntegerInRange = (name: string, value: number, min: number, max: number) => {
|
||||
if (!Number.isInteger(value)) return `${name} must be a whole number.`
|
||||
if (value < min || value > max) return `${name} must be between ${min} and ${max}.`
|
||||
return
|
||||
}
|
||||
|
||||
const createSpeciesValidators = (
|
||||
editData: EditDataType<SpeciesDetailsType>
|
||||
): Validators<Partial<EditDataType<SpeciesDetailsType>>> => ({
|
||||
subclass_or_superorder_name: {
|
||||
name: 'Subclass or Superorder',
|
||||
asString: (subClassName: string) => {
|
||||
if (subClassName.indexOf(' ') !== -1) return 'Subclass must not contain any spaces.'
|
||||
if (subClassName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Subclass must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
order_name: {
|
||||
name: 'Order',
|
||||
required: true,
|
||||
asString: (orderName: string) => {
|
||||
if (orderName !== 'incertae sedis' && orderName.indexOf(' ') !== -1)
|
||||
return 'Order must not contain any spaces, unless the value is "incertae sedis".'
|
||||
if (orderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Order must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
suborder_or_superfamily_name: {
|
||||
name: 'Suborder or Superfamily',
|
||||
asString: (subOrderName: string) => {
|
||||
if (subOrderName.indexOf(' ') !== -1) return 'Suborder must not contain any spaces.'
|
||||
if (subOrderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Suborder must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
family_name: {
|
||||
name: 'Family',
|
||||
required: true,
|
||||
asString: (familyName: string) => {
|
||||
if (familyName !== 'incertae sedis' && familyName.indexOf(' ') !== -1)
|
||||
return 'Family must not contain any spaces, unless the value is "incertae sedis".'
|
||||
if (familyName !== 'indet.' && editData.order_name === 'indet.')
|
||||
return 'when the Family is indet., Genus must also be indet.'
|
||||
if (familyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Family must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
subfamily_name: {
|
||||
name: 'Subfamily or Tribe',
|
||||
asString: (subFamilyName: string) => {
|
||||
if (subFamilyName.indexOf(' ') !== -1) return 'Subfamily must not contain any spaces.'
|
||||
if (subFamilyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Subfamily must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
genus_name: {
|
||||
name: 'Genus',
|
||||
required: true,
|
||||
asString: (genusName: string) => {
|
||||
if (genusName.indexOf(' ') !== -1) return 'Genus must not contain any spaces.'
|
||||
if (genusName !== 'indet.' && editData.family_name === 'indet.')
|
||||
return 'when the Family is indet., Genus must also be indet.'
|
||||
if (genusName !== 'gen.' && editData.family_name === 'fam.') return 'when the Family is fam., Genus must be gen.'
|
||||
if (genusName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Genus must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
species_name: {
|
||||
name: 'Species',
|
||||
required: true,
|
||||
asString: (speciesName: string) => {
|
||||
if (speciesName.indexOf(' ') !== -1) return 'Species name must not contain any spaces.'
|
||||
if (speciesName !== 'indet.' && editData.genus_name === 'indet.')
|
||||
return 'when the Genus is indet., Species must also be indet.'
|
||||
if (speciesName !== 'sp.' && editData.genus_name === 'gen.') return 'when the Genus is gen., Species must be sp.'
|
||||
if (speciesName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Species must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
taxonomic_status: {
|
||||
name: 'Taxonomic Status',
|
||||
asString: (taxonomicStatus: string) => {
|
||||
const allowedTaxonStatuses = taxonStatusOptions
|
||||
.slice(1)
|
||||
.filter((option): option is string => typeof option === 'string')
|
||||
|
||||
if (!allowedTaxonStatuses.includes(taxonomicStatus) && taxonomicStatus !== '')
|
||||
return `Taxonomic Status must be one of the following (or left empty): ${allowedTaxonStatuses.join(', ')}.`
|
||||
return
|
||||
},
|
||||
},
|
||||
body_mass: {
|
||||
name: 'Body Mass (g)',
|
||||
asNumber: (value: number) => validatePositiveInteger('Body Mass (g)', value),
|
||||
},
|
||||
brain_mass: {
|
||||
name: 'Brain Mass (g)',
|
||||
asNumber: (value: number) => validatePositiveInteger('Brain Mass (g)', value),
|
||||
},
|
||||
mw_or_low: {
|
||||
name: 'Cusp Relief Low (OR%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Relief Low (OR%)', value, 0, 100),
|
||||
},
|
||||
mw_or_high: {
|
||||
name: 'Cusp Relief High (OR%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Relief High (OR%)', value, 0, 100),
|
||||
},
|
||||
mw_cs_sharp: {
|
||||
name: 'Cusp Shape Sharp (CS%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Sharp (CS%)', value, 0, 100),
|
||||
},
|
||||
mw_cs_round: {
|
||||
name: 'Cusp Shape Rounded (CS%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Rounded (CS%)', value, 0, 100),
|
||||
},
|
||||
mw_cs_blunt: {
|
||||
name: 'Cusp Shape Blunt (CS%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Blunt (CS%)', value, 0, 100),
|
||||
},
|
||||
|
||||
mw_scale_min: {
|
||||
name: 'Scale Minimum',
|
||||
asNumber: (value: number) => {
|
||||
const nonNegativeError = validateNonNegativeInteger('Scale Minimum', value)
|
||||
if (nonNegativeError) return nonNegativeError
|
||||
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
||||
return 'Scale Minimum cannot be greater than Scale Maximum.'
|
||||
return
|
||||
},
|
||||
},
|
||||
mw_scale_max: {
|
||||
name: 'Scale Maximum',
|
||||
asNumber: (value: number) => {
|
||||
const nonNegativeError = validateNonNegativeInteger('Scale Maximum', value)
|
||||
if (nonNegativeError) return nonNegativeError
|
||||
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
||||
return 'Scale Maximum cannot be less than Scale Minimum.'
|
||||
return
|
||||
},
|
||||
},
|
||||
mw_value: {
|
||||
name: 'Reported Value',
|
||||
asNumber: (value: number) => {
|
||||
if (value < 0) return 'Reported Value cannot be negative.'
|
||||
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
||||
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
||||
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
||||
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
||||
return
|
||||
},
|
||||
},
|
||||
unique_identifier: {
|
||||
name: 'Unique Identifier',
|
||||
required: true,
|
||||
asString: (uniqueIdentifier: string) => {
|
||||
if (editData.species_name === 'sp.' && isEmptyUniqIdentifier(uniqueIdentifier ?? ''))
|
||||
return 'when the species is sp., Unique Identifier must have a value.'
|
||||
return
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const validateSpecies = (
|
||||
editData: EditDataType<SpeciesDetailsType>,
|
||||
fieldName: keyof EditDataType<SpeciesDetailsType>
|
||||
) => {
|
||||
const validatePositiveInteger = (name: string, value: number) => {
|
||||
if (!Number.isInteger(value) || value < 1) return `${name} must be a positive integer.`
|
||||
return
|
||||
}
|
||||
|
||||
const validateNonNegativeInteger = (name: string, value: number) => {
|
||||
if (!Number.isInteger(value) || value < 0) return `${name} must be a non-negative integer.`
|
||||
return
|
||||
}
|
||||
|
||||
const validateIntegerInRange = (name: string, value: number, min: number, max: number) => {
|
||||
if (!Number.isInteger(value)) return `${name} must be a whole number.`
|
||||
if (value < min || value > max) return `${name} must be between ${min} and ${max}.`
|
||||
return
|
||||
}
|
||||
|
||||
const validators: Validators<Partial<EditDataType<SpeciesDetailsType>>> = {
|
||||
subclass_or_superorder_name: {
|
||||
name: 'Subclass or Superorder',
|
||||
asString: (subClassName: string) => {
|
||||
if (subClassName.indexOf(' ') !== -1) return 'Subclass must not contain any spaces.'
|
||||
if (subClassName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Subclass must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
order_name: {
|
||||
name: 'Order',
|
||||
required: true,
|
||||
asString: (orderName: string) => {
|
||||
if (orderName !== 'incertae sedis' && orderName.indexOf(' ') !== -1)
|
||||
return 'Order must not contain any spaces, unless the value is "incertae sedis".'
|
||||
if (orderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Order must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
suborder_or_superfamily_name: {
|
||||
name: 'Suborder or Superfamily',
|
||||
asString: (subOrderName: string) => {
|
||||
if (subOrderName.indexOf(' ') !== -1) return 'Suborder must not contain any spaces.'
|
||||
if (subOrderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Suborder must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
family_name: {
|
||||
name: 'Family',
|
||||
required: true,
|
||||
asString: (familyName: string) => {
|
||||
if (familyName !== 'incertae sedis' && familyName.indexOf(' ') !== -1)
|
||||
return 'Family must not contain any spaces, unless the value is "incertae sedis".'
|
||||
if (familyName !== 'indet.' && editData.order_name === 'indet.')
|
||||
return 'when the Family is indet., Genus must also be indet.'
|
||||
if (familyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Family must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
subfamily_name: {
|
||||
name: 'Subfamily or Tribe',
|
||||
asString: (subFamilyName: string) => {
|
||||
if (subFamilyName.indexOf(' ') !== -1) return 'Subfamily must not contain any spaces.'
|
||||
if (subFamilyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Subfamily must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
genus_name: {
|
||||
name: 'Genus',
|
||||
required: true,
|
||||
asString: (genusName: string) => {
|
||||
if (genusName.indexOf(' ') !== -1) return 'Genus must not contain any spaces.'
|
||||
if (genusName !== 'indet.' && editData.family_name === 'indet.')
|
||||
return 'when the Family is indet., Genus must also be indet.'
|
||||
if (genusName !== 'gen.' && editData.family_name === 'fam.')
|
||||
return 'when the Family is fam., Genus must be gen.'
|
||||
if (genusName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Genus must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
species_name: {
|
||||
name: 'Species',
|
||||
required: true,
|
||||
asString: (speciesName: string) => {
|
||||
if (speciesName.indexOf(' ') !== -1) return 'Species name must not contain any spaces.'
|
||||
if (speciesName !== 'indet.' && editData.genus_name === 'indet.')
|
||||
return 'when the Genus is indet., Species must also be indet.'
|
||||
if (speciesName !== 'sp.' && editData.genus_name === 'gen.')
|
||||
return 'when the Genus is gen., Species must be sp.'
|
||||
if (speciesName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||
return 'Species must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||
return
|
||||
},
|
||||
},
|
||||
taxonomic_status: {
|
||||
name: 'Taxonomic Status',
|
||||
asString: (taxonomicStatus: string) => {
|
||||
const allowedTaxonStatuses = taxonStatusOptions
|
||||
.slice(1)
|
||||
.filter((option): option is string => typeof option === 'string')
|
||||
|
||||
if (!allowedTaxonStatuses.includes(taxonomicStatus) && taxonomicStatus !== '')
|
||||
return `Taxonomic Status must be one of the following (or left empty): ${allowedTaxonStatuses.join(', ')}.`
|
||||
return
|
||||
},
|
||||
},
|
||||
body_mass: {
|
||||
name: 'Body Mass (g)',
|
||||
asNumber: (value: number) => validatePositiveInteger('Body Mass (g)', value),
|
||||
},
|
||||
brain_mass: {
|
||||
name: 'Brain Mass (g)',
|
||||
asNumber: (value: number) => validatePositiveInteger('Brain Mass (g)', value),
|
||||
},
|
||||
mw_or_low: {
|
||||
name: 'Cusp Relief Low (OR%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Relief Low (OR%)', value, 0, 100),
|
||||
},
|
||||
mw_or_high: {
|
||||
name: 'Cusp Relief High (OR%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Relief High (OR%)', value, 0, 100),
|
||||
},
|
||||
mw_cs_sharp: {
|
||||
name: 'Cusp Shape Sharp (CS%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Sharp (CS%)', value, 0, 100),
|
||||
},
|
||||
mw_cs_round: {
|
||||
name: 'Cusp Shape Rounded (CS%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Rounded (CS%)', value, 0, 100),
|
||||
},
|
||||
mw_cs_blunt: {
|
||||
name: 'Cusp Shape Blunt (CS%)',
|
||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Blunt (CS%)', value, 0, 100),
|
||||
},
|
||||
|
||||
mw_scale_min: {
|
||||
name: 'Scale Minimum',
|
||||
asNumber: (value: number) => {
|
||||
const nonNegativeError = validateNonNegativeInteger('Scale Minimum', value)
|
||||
if (nonNegativeError) return nonNegativeError
|
||||
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
||||
return 'Scale Minimum cannot be greater than Scale Maximum.'
|
||||
return
|
||||
},
|
||||
},
|
||||
mw_scale_max: {
|
||||
name: 'Scale Maximum',
|
||||
asNumber: (value: number) => {
|
||||
const nonNegativeError = validateNonNegativeInteger('Scale Maximum', value)
|
||||
if (nonNegativeError) return nonNegativeError
|
||||
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
||||
return 'Scale Maximum cannot be less than Scale Minimum.'
|
||||
return
|
||||
},
|
||||
},
|
||||
mw_value: {
|
||||
name: 'Reported Value',
|
||||
asNumber: (value: number) => {
|
||||
if (value < 0) return 'Reported Value cannot be negative.'
|
||||
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
||||
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
||||
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
||||
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
||||
return
|
||||
},
|
||||
},
|
||||
unique_identifier: {
|
||||
name: 'Unique Identifier',
|
||||
required: true,
|
||||
asString: (uniqueIdentifier: string) => {
|
||||
if (editData.species_name === 'sp.' && isEmptyUniqIdentifier(uniqueIdentifier ?? ''))
|
||||
return 'when the species is sp., Unique Identifier must have a value.'
|
||||
return
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const validators = createSpeciesValidators(editData)
|
||||
return validator<EditDataType<SpeciesDetailsType>>(validators, editData, fieldName)
|
||||
}
|
||||
|
||||
export const validateSpeciesFields = (editData: Partial<EditDataType<SpeciesDetailsType>>) =>
|
||||
validateFields<EditDataType<SpeciesDetailsType>>(
|
||||
createSpeciesValidators(editData as EditDataType<SpeciesDetailsType>),
|
||||
editData
|
||||
)
|
||||
|
||||
@@ -1,64 +1,74 @@
|
||||
import { EditDataType, TimeUnitDetailsType } from '../types'
|
||||
import { Validators, validator } from './validator'
|
||||
import { Validators, validateFields, validator } from './validator'
|
||||
|
||||
const createTimeUnitValidators = (
|
||||
editData: EditDataType<TimeUnitDetailsType>
|
||||
): Validators<EditDataType<Partial<TimeUnitDetailsType>>> => ({
|
||||
tu_display_name: {
|
||||
name: 'Name',
|
||||
required: true,
|
||||
},
|
||||
sequence: {
|
||||
name: 'Sequence',
|
||||
required: true,
|
||||
},
|
||||
up_bnd: {
|
||||
name: 'New Upper Bound',
|
||||
required: true,
|
||||
asNumber: (num: number) => {
|
||||
if (num === editData.low_bnd) return 'Upper and lower bounds cannot be the same'
|
||||
return
|
||||
},
|
||||
},
|
||||
low_bnd: {
|
||||
name: 'New Lower Bound',
|
||||
required: true,
|
||||
asNumber: (num: number) => {
|
||||
if (num === editData.up_bnd) return 'Upper and lower bounds cannot be the same'
|
||||
return
|
||||
},
|
||||
},
|
||||
up_bound: {
|
||||
name: 'Upper Bound',
|
||||
required: () =>
|
||||
editData.up_bnd !== undefined && editData.up_bnd !== null
|
||||
? `Upper bound with ID ${editData.up_bnd} does not exist`
|
||||
: 'This field is required',
|
||||
miscCheck: () => {
|
||||
if (editData.low_bound && editData.low_bound.age! === editData.up_bound!.age!) {
|
||||
return 'Upper bound age cannot be the same as lower bound age'
|
||||
}
|
||||
if (editData.low_bound && editData.low_bound.age! < editData.up_bound!.age!) {
|
||||
return 'Upper bound age has to be lower than lower bound age'
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
low_bound: {
|
||||
name: 'Lower Bound',
|
||||
required: () =>
|
||||
editData.low_bnd !== undefined && editData.low_bnd !== null
|
||||
? `Lower bound with ID ${editData.low_bnd} does not exist`
|
||||
: 'This field is required',
|
||||
miscCheck: () => {
|
||||
if (editData.up_bound && editData.up_bound.age! === editData.low_bound!.age!) {
|
||||
return 'Lower bound age cannot be the same as upper bound age'
|
||||
}
|
||||
if (editData.up_bound && editData.up_bound.age! > editData.low_bound!.age!) {
|
||||
return 'Lower bound age has to be higher than upper bound age'
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const validateTimeUnit = (editData: EditDataType<TimeUnitDetailsType>, fieldName: keyof TimeUnitDetailsType) => {
|
||||
const validators: Validators<EditDataType<Partial<TimeUnitDetailsType>>> = {
|
||||
tu_display_name: {
|
||||
name: 'Name',
|
||||
required: true,
|
||||
},
|
||||
sequence: {
|
||||
name: 'Sequence',
|
||||
required: true,
|
||||
},
|
||||
up_bnd: {
|
||||
name: 'New Upper Bound',
|
||||
required: true,
|
||||
asNumber: (num: number) => {
|
||||
if (num === editData.low_bnd) return 'Upper and lower bounds cannot be the same'
|
||||
return
|
||||
},
|
||||
},
|
||||
low_bnd: {
|
||||
name: 'New Lower Bound',
|
||||
required: true,
|
||||
asNumber: (num: number) => {
|
||||
if (num === editData.up_bnd) return 'Upper and lower bounds cannot be the same'
|
||||
return
|
||||
},
|
||||
},
|
||||
up_bound: {
|
||||
name: 'Upper Bound',
|
||||
required: () =>
|
||||
editData.up_bnd !== undefined && editData.up_bnd !== null
|
||||
? `Upper bound with ID ${editData.up_bnd} does not exist`
|
||||
: 'This field is required',
|
||||
miscCheck: () => {
|
||||
if (editData.low_bound && editData.low_bound.age! === editData.up_bound!.age!) {
|
||||
return 'Upper bound age cannot be the same as lower bound age'
|
||||
}
|
||||
if (editData.low_bound && editData.low_bound.age! < editData.up_bound!.age!) {
|
||||
return 'Upper bound age has to be lower than lower bound age'
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
low_bound: {
|
||||
name: 'Lower Bound',
|
||||
required: () =>
|
||||
editData.low_bnd !== undefined && editData.low_bnd !== null
|
||||
? `Lower bound with ID ${editData.low_bnd} does not exist`
|
||||
: 'This field is required',
|
||||
miscCheck: () => {
|
||||
if (editData.up_bound && editData.up_bound.age! === editData.low_bound!.age!) {
|
||||
return 'Lower bound age cannot be the same as upper bound age'
|
||||
}
|
||||
if (editData.up_bound && editData.up_bound.age! > editData.low_bound!.age!) {
|
||||
return 'Lower bound age has to be higher than upper bound age'
|
||||
}
|
||||
return
|
||||
},
|
||||
},
|
||||
}
|
||||
const validators = createTimeUnitValidators(editData)
|
||||
return validator<EditDataType<TimeUnitDetailsType>>(validators, editData, fieldName)
|
||||
}
|
||||
|
||||
export const validateTimeUnitFields = (editData: Partial<EditDataType<TimeUnitDetailsType>>) =>
|
||||
validateFields<EditDataType<TimeUnitDetailsType>>(
|
||||
createTimeUnitValidators(editData as EditDataType<TimeUnitDetailsType>),
|
||||
editData
|
||||
)
|
||||
|
||||
@@ -167,6 +167,7 @@ jest.mock('@/util/taxonomyUtilities', () => ({
|
||||
|
||||
jest.mock('@/shared/validators/species', () => ({
|
||||
validateSpecies: jest.fn(() => ({ name: '', error: null })),
|
||||
validateSpeciesFields: jest.fn(() => []),
|
||||
}))
|
||||
|
||||
const mockUseDetailContext = useDetailContext as jest.MockedFunction<() => DetailContextType<LocalityDetailsType>>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, it } from '@jest/globals'
|
||||
|
||||
import { validateLocalityFields } from '@/shared/validators/locality'
|
||||
import { validateMuseumFields } from '@/shared/validators/museum'
|
||||
import { validatePersonFields } from '@/shared/validators/person'
|
||||
import { validateRegionFields } from '@/shared/validators/region'
|
||||
import { validateSpeciesFields } from '@/shared/validators/species'
|
||||
import { validateTimeBoundFields } from '@/shared/validators/timeBound'
|
||||
import { validateTimeUnitFields } from '@/shared/validators/timeUnit'
|
||||
import { validateFields, type Validators } from '@/shared/validators/validator'
|
||||
|
||||
type TestData = {
|
||||
@@ -43,3 +49,74 @@ describe('validateTimeBoundFields', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('entity full-field validators', () => {
|
||||
it('reports missing Time Unit required fields', () => {
|
||||
const errors = validateTimeUnitFields({})
|
||||
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'Name', error: 'This field is required' },
|
||||
{ name: 'Sequence', error: 'This field is required' },
|
||||
{ name: 'New Upper Bound', error: 'This field is required' },
|
||||
{ name: 'New Lower Bound', error: 'This field is required' },
|
||||
{ name: 'Upper Bound', error: 'This field is required' },
|
||||
{ name: 'Lower Bound', error: 'This field is required' },
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('reports missing simple entity required fields', () => {
|
||||
expect(validatePersonFields({})).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'initials', error: 'This field is required' },
|
||||
{ name: 'First Name', error: 'This field is required' },
|
||||
{ name: 'Country', error: 'This field is required' },
|
||||
])
|
||||
)
|
||||
|
||||
expect(validateMuseumFields({})).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'Museum', error: 'This field is required' },
|
||||
{ name: 'Institution', error: 'This field is required' },
|
||||
{ name: 'Country', error: 'This field is required' },
|
||||
])
|
||||
)
|
||||
|
||||
expect(validateRegionFields({})).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'reg_coord_id', error: 'This field is required' },
|
||||
{ name: 'Region', error: 'This field is required' },
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('reports missing Species taxonomy fields', () => {
|
||||
const errors = validateSpeciesFields({})
|
||||
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'Order', error: 'This field is required' },
|
||||
{ name: 'Family', error: 'This field is required' },
|
||||
{ name: 'Genus', error: 'This field is required' },
|
||||
{ name: 'Species', error: 'This field is required' },
|
||||
{ name: 'Unique Identifier', error: 'This field is required' },
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('reports missing Locality required and conditional fields', () => {
|
||||
const errors = validateLocalityFields({ date_meth: 'time_unit' })
|
||||
|
||||
expect(errors).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'Age (max)', error: 'This field is required' },
|
||||
{ name: 'Age (min)', error: 'This field is required' },
|
||||
{ name: 'Basis for age (Time unit, min)', error: 'This field is required' },
|
||||
{ name: 'Basis for age (Time unit, max)', error: 'This field is required' },
|
||||
{ name: 'Locality name', error: 'This field is required' },
|
||||
{ name: 'Country', error: 'This field is required' },
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,12 +9,20 @@ import { PageContextProvider } from '@/components/Page'
|
||||
import type { TimeUnit, TimeUnitDetailsType } from '@/shared/types'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
jest.setTimeout(15000)
|
||||
|
||||
jest.mock('lodash-es', () => ({
|
||||
cloneDeep: (value: unknown) => value,
|
||||
}))
|
||||
|
||||
jest.mock('@/shared/validators/timeUnit', () => ({
|
||||
validateTimeUnit: () => ({ name: 'tu_display_name', error: '' }),
|
||||
validateTimeUnitFields: () => [],
|
||||
}))
|
||||
|
||||
jest.mock('@/shared/validators/validator', () => ({
|
||||
...jest.requireActual<typeof import('@/shared/validators/validator')>('@/shared/validators/validator'),
|
||||
referenceValidator: () => null,
|
||||
}))
|
||||
|
||||
jest.mock('@/util/config', () => ({
|
||||
@@ -23,6 +31,10 @@ jest.mock('@/util/config', () => ({
|
||||
ENV: 'test',
|
||||
}))
|
||||
|
||||
jest.mock('@/hooks/user', () => ({
|
||||
useUser: () => ({ role: 1, initials: 'TEST' }),
|
||||
}))
|
||||
|
||||
const mockEditTimeUnitMutation = jest.fn()
|
||||
const mockGetAllTimeUnitsQuery = jest.fn()
|
||||
|
||||
@@ -38,6 +50,17 @@ jest.mock('@/redux/timeUnitReducer', () => ({
|
||||
useGetAllTimeUnitsQuery: (...args: unknown[]) => mockGetAllTimeUnitsQuery(...args),
|
||||
}))
|
||||
|
||||
jest.mock('@/redux/speciesReducer', () => ({
|
||||
useGetAllSpeciesQuery: () => ({ data: undefined, isFetching: false, isLoading: false }),
|
||||
useGetAllSynonymsQuery: () => ({ data: undefined, isFetching: false, isLoading: false }),
|
||||
}))
|
||||
|
||||
jest.mock('@/redux/referenceReducer', () => ({
|
||||
useGetAllReferencesQuery: () => ({ data: [], isError: false }),
|
||||
useGetReferenceTypesQuery: () => ({ data: [] }),
|
||||
useEditReferenceMutation: () => [jest.fn(), { isLoading: false }],
|
||||
}))
|
||||
|
||||
jest.mock('@/components/Sequence/SequenceSelect', () => ({
|
||||
SequenceSelect: () => <div data-testid="sequence-select" />,
|
||||
}))
|
||||
@@ -178,10 +201,13 @@ describe('TimeUnit creation duplicate name handling', () => {
|
||||
return { unwrap: unwrapMock }
|
||||
})
|
||||
|
||||
renderTimeUnitCreation()
|
||||
const { container } = renderTimeUnitCreation()
|
||||
|
||||
const user = userEvent.setup()
|
||||
|
||||
const nameInput = container.querySelector('input#tu_display_name-textfield') as HTMLInputElement
|
||||
await user.type(nameInput, 'Rank Test Time Unit')
|
||||
|
||||
const rankSelect = screen.getByLabelText('Rank')
|
||||
await user.click(rankSelect)
|
||||
await user.click(await screen.findByText('No value'))
|
||||
|
||||
Reference in New Issue
Block a user