Skip to content

Commit

Permalink
[feat]: Add question mark validation to question title form (ONEARMY#…
Browse files Browse the repository at this point in the history
…3758)

* feat: add question mark validation to input and change composeValidators to return multiple errors
* test: add value validator to noSpecialCharacters
* test: increase research routes custom timeouts
  • Loading branch information
lucaasrojas authored Jul 12, 2024
1 parent 8f75c79 commit 497d10b
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { fields } from 'src/pages/Question/labels'
import {
composeValidators,
endsWithQuestionMark,
minValue,
required,
validateTitle,
Expand Down Expand Up @@ -38,6 +39,7 @@ export const QuestionTitleField = (props: IProps) => {
validate={composeValidators(
required,
minValue(QUESTION_MIN_TITLE_LENGTH),
endsWithQuestionMark(),
validateTitle(parentType, questionId, questionStore),
)}
component={FieldInput}
Expand Down
155 changes: 106 additions & 49 deletions src/pages/Research/research.routes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ describe('research.routes', () => {
wrapper.getByText(/Help out with Research & Development/),
).toBeInTheDocument(),
{
timeout: 2000,
timeout: 10000,
},
)
})
Expand All @@ -165,7 +165,7 @@ describe('research.routes', () => {
)
},
{
timeout: 5000,
timeout: 10000,
},
)
})
Expand All @@ -179,11 +179,16 @@ describe('research.routes', () => {
wrapper = renderFn('/research/create')
})

await waitFor(() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
})
await waitFor(
() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('rejects a logged in user missing required role', async () => {
Expand All @@ -192,11 +197,16 @@ describe('research.routes', () => {
wrapper = renderFn('/research/create')
})

await waitFor(() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
})
await waitFor(
() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('accepts a logged in user with required role [research_creator]', async () => {
Expand All @@ -212,7 +222,7 @@ describe('research.routes', () => {
expect(wrapper.getByText(/start your research/i)).toBeInTheDocument()
},
{
timeout: 2000,
timeout: 10000,
},
)
})
Expand All @@ -229,7 +239,7 @@ describe('research.routes', () => {
expect(wrapper.getByText(/start your research/i)).toBeInTheDocument()
},
{
timeout: 2000,
timeout: 10000,
},
)
})
Expand All @@ -244,11 +254,16 @@ describe('research.routes', () => {
wrapper = renderFn('/research/an-example/edit')
})

await waitFor(() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
})
await waitFor(
() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('accepts a logged in user with required role', async () => {
Expand Down Expand Up @@ -285,9 +300,16 @@ describe('research.routes', () => {
renderFn('/research/an-example/edit')
})

await waitFor(() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith('/research/an-example')
})
await waitFor(
() => {
expect(mockedUsedNavigate).toHaveBeenCalledWith(
'/research/an-example',
)
},
{
timeout: 10000,
},
)
})

it('blocks a valid editor when document is locked by another user', async () => {
Expand All @@ -310,13 +332,18 @@ describe('research.routes', () => {
wrapper = renderFn('/research/an-example/edit')
})

await waitFor(() => {
expect(
wrapper.getByText(
'The research description is currently being edited by another editor.',
),
).toBeInTheDocument()
})
await waitFor(
() => {
expect(
wrapper.getByText(
'The research description is currently being edited by another editor.',
),
).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('accepts a user when document is mark locked by them', async () => {
Expand All @@ -339,9 +366,14 @@ describe('research.routes', () => {
wrapper = renderFn('/research/an-example/edit')
})

await waitFor(() => {
expect(wrapper.getByText('Edit your Research')).toBeInTheDocument()
})
await waitFor(
() => {
expect(wrapper.getByText('Edit your Research')).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('accepts a user with required role and contributor acccess', async () => {
Expand Down Expand Up @@ -378,11 +410,16 @@ describe('research.routes', () => {
wrapper = renderFn('/research/an-example/new-update')
})

await waitFor(() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
})
await waitFor(
() => {
expect(
wrapper.getByText(/role required to access this page/),
).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('accepts a logged in user with required role', async () => {
Expand All @@ -392,9 +429,14 @@ describe('research.routes', () => {
wrapper = renderFn('/research/an-example/new-update')
})

await waitFor(() => {
expect(wrapper.getByTestId('EditResearchUpdate')).toBeInTheDocument()
})
await waitFor(
() => {
expect(wrapper.getByTestId('EditResearchUpdate')).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})
})

Expand Down Expand Up @@ -437,9 +479,14 @@ describe('research.routes', () => {
)
})

await waitFor(() => {
expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument()
})
await waitFor(
() => {
expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('blocks valid author when document is locked', async () => {
Expand Down Expand Up @@ -507,9 +554,14 @@ describe('research.routes', () => {
)
})

await waitFor(() => {
expect(wrapper.getByText('Edit your update')).toBeInTheDocument()
})
await waitFor(
() => {
expect(wrapper.getByText('Edit your update')).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})

it('rejects logged in user who is not author', async () => {
Expand Down Expand Up @@ -554,9 +606,14 @@ describe('research.routes', () => {
)
})

await waitFor(() => {
expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument()
})
await waitFor(
() => {
expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument()
},
{
timeout: 10000,
},
)
})
})
})
Expand Down
42 changes: 42 additions & 0 deletions src/utils/validators.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { afterEach, describe, expect, it, vi } from 'vitest'

import {
composeValidators,
draftValidationWrapper,
endsWithQuestionMark,
minValue,
noSpecialCharacters,
validateTitle,
} from './validators'

// Mock out module store to limit impact of circular dependency
vi.mock('src/stores/common/module.store')

import { QUESTION_MIN_TITLE_LENGTH } from 'src/pages/Question/constants'

import { ResearchStore } from '../stores/Research/research.store'

import type { IRootStore } from 'src/stores/RootStore'
Expand Down Expand Up @@ -84,3 +89,40 @@ describe('noSpecialCharacters', () => {
expect(result).toContain('Only letters and numbers are allowed')
})
})

describe('endsWithQuestionMark', () => {
const errorMessage = 'Needs to end with a question mark'
it('returns proper message when there are more than one question mark at the end', () => {
const result = endsWithQuestionMark()
expect(result('this is my question??')).toContain(errorMessage)
})

it('returns proper message when there are more than one question mark', () => {
const result = endsWithQuestionMark()
expect(result('this is my qu?estion?')).toContain(errorMessage)
})

it('returns proper message when there is no question mark', () => {
const result = endsWithQuestionMark()
expect(result('this is my question')).toContain(errorMessage)
})

it('returns proper message when there the question mark is not at the end', () => {
const result = endsWithQuestionMark()
expect(result('this is my? question')).toContain(errorMessage)
})
})

describe('composeValidators', () => {
it('should join multiple error messages', async () => {
const composedValidatorsFunc = composeValidators(
minValue(QUESTION_MIN_TITLE_LENGTH),
endsWithQuestionMark(),
)
const messages = await composedValidatorsFunc('this is')
expect(messages).toContain('Needs to end with a question mark')
expect(messages).toContain(
`Should be more than ${QUESTION_MIN_TITLE_LENGTH} characters`,
)
})
})
24 changes: 19 additions & 5 deletions src/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const required = (value: any) =>
value ? undefined : 'Make sure this field is filled correctly'

const noSpecialCharacters = (value: string) => {
const specialCharacters = getSpecialCharacters(value)
const specialCharacters = value ? getSpecialCharacters(value) : ''
return specialCharacters.length > 0
? 'Only letters and numbers are allowed'
: undefined
Expand All @@ -40,14 +40,27 @@ const minValue = (min: number) => (value) => {
: undefined
}

const endsWithQuestionMark = () => (value) => {
const index = value.indexOf('?')
return index < 0 || index < value.length - 1
? 'Needs to end with a question mark'
: undefined
}

const composeValidators =
(...validators) =>
(value) =>
validators.reduce(
(error, validator) => error || validator(value),
undefined,
async (value) => {
const allResponse = await Promise.all(
validators.map((validator) => validator(value)),
)

return allResponse.reduce(
(message, value) =>
typeof value === 'string' ? (message += value + '. ') : message,
'',
)
}

const validateUrl = (value: any) => {
if (value) {
return isUrl(value) ? undefined : 'Invalid url'
Expand Down Expand Up @@ -137,4 +150,5 @@ export {
composeValidators,
validateTitle,
noSpecialCharacters,
endsWithQuestionMark,
}

0 comments on commit 497d10b

Please sign in to comment.