diff --git a/jest.config.js b/jest.config.js index 8e0e44a60c4c9604843f2b326369c1145ea8c215..7e3659738234237b5cdaff14783d201e350a035e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,3 +4,4 @@ module.exports = { '\\.[as]?css$': 'identity-obj-proxy', }, }; + diff --git a/src/App.tsx b/src/App.tsx index 58ac19afb39e681d30e7f62121396800f9a4c222..696c6c4220b5b97631dc53c429b1a6677f74c976 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import type { Problem, Suggestion } from './types'; import ContactForm from './components/contact-form/ContactForm'; import styles from './App.module.scss'; -function App(): JSX.Element { +export const App: React.FC = () => { const { t } = useTranslation(); const [problemGroup, _setProblemGroup] = useState<string>(''); const [problem, setProblem] = useState<string>(''); @@ -70,7 +70,7 @@ function App(): JSX.Element { <Markdown>{t('subtitle')}</Markdown> </p> </header> - <form className={styles.troubleshooting}> + <form className={styles.troubleshooting} aria-label={t("Troubleshooting form")}> <ProblemGroupDropdown id="problemGroup" intro={localized.problemDescribe} diff --git a/src/__mocks__/Modal.tsx b/src/__mocks__/Modal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2ced19791d471df112345b018574e111c69b6c3f --- /dev/null +++ b/src/__mocks__/Modal.tsx @@ -0,0 +1,48 @@ +import React, { useEffect } from 'react'; +import type { ModalProps } from '../components/modal/Modal'; + +export const Modal = ({ + children, + DOMNode = 'body', + curtain = true, + closeCallback, + defaultActionCallback, + closeButton, + header, + buttons, +}: ModalProps): JSX.Element => { + const keyUpHandler = (event: KeyboardEvent) => { + if (event.key == 'Escape' && typeof closeCallback == 'function') + closeCallback(); + if (event.key == 'Enter' && typeof defaultActionCallback == 'function') + defaultActionCallback(); + }; + + useEffect(() => { + window.addEventListener('keyup', keyUpHandler); + return () => { + window.removeEventListener('keyup', keyUpHandler); + }; + }); + const close = () => typeof closeCallback == 'function' && closeCallback(); + return ( + <div className="modal"> + <header> + <h5>{header}</h5> + {closeButton && ( + <button className="close" onClick={close}> + ⨯ + </button> + )} + </header> + <main>{children}</main> + <footer>{buttons}</footer> + <div> + {typeof DOMNode == 'string' ? DOMNode : JSON.stringify(DOMNode)} -{' '} + {curtain ? 'curtain' : ''} + </div> + </div> + ); +}; + +export default Modal; diff --git a/src/components/contact-form/ContactForm.test.tsx b/src/components/contact-form/ContactForm.test.tsx index 87e93f62b3d479cfc375c3d26cc278e156040cc5..0a3379e3a1bc1f63cd257b135708382baa8a0c9b 100644 --- a/src/components/contact-form/ContactForm.test.tsx +++ b/src/components/contact-form/ContactForm.test.tsx @@ -1,41 +1,143 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; import ContactForm from './ContactForm'; +import { config as transitionConfig } from 'react-transition-group'; import userEvent from '@testing-library/user-event'; +import { fireEvent, render, screen } from '@testing-library/react'; +// Disable CSSTransitions to prevent them from messing with test conditions +transitionConfig.disabled = true; -const mockSetUrgentIntent = jest.fn(); -const mockSendUrgent = jest.fn(); const problem = ''; // for now makes no difference -describe.each([ - ['', false], - ['administrative', false], - ['technical', false], - ['technical', true], -])('<ContactForm problemGroup={%s}>', (problemGroup) => { - it(`renders the contact form with problemGroup: ${problemGroup}`, () => { +const fillInContactForm = () => { + const nameInput = screen.getByRole('textbox', { name: /name/i }); + const phoneInput = screen.getByRole('textbox', { name: /phone/i }); + const emailInput = screen.getByRole('textbox', { name: /e-?mail/i }); + const messageInput = screen.getByRole('textbox', { name: /message/i }); + + expect(nameInput).toBeInTheDocument(); + expect(phoneInput).toBeInTheDocument(); + expect(emailInput).toBeInTheDocument(); + expect(messageInput).toBeInTheDocument(); + + // Note, fields are checked server side so don't expect validation here. + userEvent.type(nameInput, 'Maurice Moss'); + userEvent.type(phoneInput, '0118 999 881 99 9119 7253'); + userEvent.type(emailInput, 'moss@itcrowd.co.uk'); + userEvent.type( + messageInput, + 'Dear sir/madam,\n\n' + + 'Fire! Fire! Help me!\n\n' + + '123, Caladin road\n\n' + + 'Looking forward to hearing from you.\n\n' + + 'All the best,\n' + + 'Maurice Moss.', + ); + // Let's have one check here.. + expect(nameInput).toHaveValue('Maurice Moss'); +}; + +describe('<ContactForm problemGroup="%s"}>', () => { + it.each([[''], ['administrative'], ['technical']])( + 'renders the contact form with problemGroup: %s', + (problemGroup) => { + const { getByRole, queryByRole, getByText } = render( + <ContactForm {...{ problemGroup, problem }} />, + ); + const contactDetailFieldset = getByRole('group', { + name: /Contact details/i, + }); + const messageFieldset = getByRole('group', { + name: /what do you want to tell us/i, + }); + + const urgencyCheckbox = queryByRole('checkbox', { + name: /urgent request/i, + }); + + if (problemGroup == 'technical') + expect(urgencyCheckbox).toBeInTheDocument(); + else expect(urgencyCheckbox).not.toBeInTheDocument(); + + const regularSubmit = getByRole('button', { name: /submit request/i }); + expect(regularSubmit).toBeInTheDocument(); + + expect(contactDetailFieldset).toBeInTheDocument(); + expect(messageFieldset).toBeInTheDocument(); + }, + ); + it.each([ + ['pressing key', '{esc}'], + ['pressing key', '{enter}'], + ['clicking button', 'Okay'], + ['clicking button', 'close'], + ])('confirmation modal closes on %s %s', (action, value) => { + const problemGroup = 'technical'; const { getByRole } = render( <ContactForm {...{ problemGroup, problem }} />, ); - const contactDetailFieldset = getByRole('group', { - name: /Contact details/i, - }); - const messageFieldset = getByRole('group', { - name: /what do you want to tell us/i, - }); const regularSubmit = getByRole('button', { name: /submit request/i }); - expect(contactDetailFieldset).toBeInTheDocument(); - expect(messageFieldset).toBeInTheDocument(); + userEvent.click(regularSubmit); + + const modal = getByRole('dialog', { name: /thank you/i }); + expect(modal).toBeInTheDocument(); + + if (/clicking/.test(action)) { + // Close modal by clicking a button.. + const button = getByRole('button', { name: value }); + userEvent.click(button); + } else { + // Close modal by pressing a key.. + const body = document.getElementsByTagName('body')[0]; + userEvent.type(body, value); + } + expect(modal).not.toBeInTheDocument(); + }); + + it('can be submitted by a form submit event', ()=>{ + const problemGroup = 'technical'; + const { getByRole } = render( + <ContactForm {...{ problemGroup, problem }} />, + ); + const form = getByRole("form", {name:"Contact form"}); + fireEvent.submit(form); + const modal = getByRole('dialog', { name: /thank you/i }); + expect(modal).toBeInTheDocument(); + }); + + it('has an option to make the request urgent', () => { + const problemGroup = 'technical'; + const { getByText, getByRole } = render( + <ContactForm {...{ problemGroup, problem }} />, + ); + const urgencyCheckbox = getByRole('checkbox', { + name: /urgent request/i, + }); + expect(urgencyCheckbox).toBeInTheDocument(); + + const regularSubmit = getByRole('button', { name: /submit request/i }); expect(regularSubmit).toBeInTheDocument(); - /** - * To test: - * - Fields are rendered - * - Fill in some fields - * - Not filling in required fields leads to errors -> integration test or mocked, or end-to-end? - * - Form submission, urgent and regular, triggers a call to a function - */ - + fillInContactForm(); + + userEvent.click(urgencyCheckbox); + + const urgencyInput = getByRole('textbox', { name: /type urgent/i }); + userEvent.type(urgencyInput, 'URGENT'); + expect(regularSubmit).not.toBeInTheDocument(); + const urgentSubmit = getByRole('button', { + name: /submit urgent request/i, + }); + expect(urgentSubmit).toBeInTheDocument(); + userEvent.click(urgentSubmit); + + const dialogMain = getByText(/fire!/i); + expect(dialogMain).toBeInTheDocument(); + // Close modal by pressing okay.. + const modal = getByRole('dialog', { name: /thank you/i }); + expect(modal).toBeInTheDocument(); + const okay = getByRole('button', { name: /okay/i }); + userEvent.click(okay); + expect(modal).not.toBeInTheDocument(); }); }); diff --git a/src/components/contact-form/ContactForm.tsx b/src/components/contact-form/ContactForm.tsx index d58b01b4e9ec481936ac4c10b3509a022a5910e0..3f8dc081945ade47761c4542e65097c3d20a2598 100644 --- a/src/components/contact-form/ContactForm.tsx +++ b/src/components/contact-form/ContactForm.tsx @@ -1,4 +1,3 @@ -import Markdown from 'markdown-to-jsx'; import React, { ChangeEvent, FormEvent, useState } from 'react'; import { useTranslation } from 'react-i18next'; import UrgentRequest from '../urgent-request/UrgentRequest'; @@ -7,15 +6,19 @@ import styles from './ContactForm.module.scss'; import panelTransition from '../../styles/transitions/panel.module.scss'; import Fieldset from '../fieldset/Fieldset'; import Input from '../input/Input'; +import Modal from '../modal/Modal'; export interface ContactFormProps { problemGroup: string; problem: string; } - -function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element { +export const ContactForm: React.FC<ContactFormProps> = ({ + problemGroup, + problem, +}: ContactFormProps) => { const [urgentIntent, setUrgentIntent] = useState<boolean>(false); const [state, setState] = useState<Record<string, string>>({}); + const [modalContent, setModalContent] = useState<string>(''); const { t } = useTranslation(); const changeHandler = ( @@ -28,10 +31,10 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element { setUrgentIntent(event.target.checked); }; - function submit(event: FormEvent<HTMLFormElement>) { + const submit = (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); sendRequest(); - } + }; const sendRequest = (urgent = false) => { const formData = { ...state, ...{ urgent: urgent ? 'yes' : 'no' } }; @@ -41,13 +44,12 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element { `SupportForm[${name}]`, value, ]), - ), - console.log(formData); + ); + setModalContent(JSON.stringify(formData)); }; return ( - <form onSubmit={submit} className={`${styles.ContactForm}`}> - + <form onSubmit={submit} className={`${styles.ContactForm}`} aria-label={t("Contact form")}> <Fieldset legend={t('contact.details')}> <Input label={t('contact.name')} @@ -108,6 +110,7 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element { mountOnEnter={true} addEndListener={(node: HTMLElement, done: EventListener) => { // use the css transitionend event to mark the finish of a transition + // istanbul ignore next node.addEventListener('transitionend', done, false); }} > @@ -117,7 +120,7 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element { <button className="good" onClick={(event) => { - event?.preventDefault(); + event.preventDefault(); sendRequest(); }} > @@ -127,8 +130,22 @@ function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element { </CSSTransition> </SwitchTransition> </div> + {modalContent && ( + <Modal + defaultActionCallback={() => setModalContent('')} + closeCallback={() => setModalContent('')} + buttons={ + <button className="good" onClick={() => setModalContent('')}> + {t('Okay')} + </button> + } + header={t('Thank you')} + > + {modalContent} + </Modal> + )} </form> ); -} +}; export default ContactForm; diff --git a/src/components/fieldset/Fieldset.tsx b/src/components/fieldset/Fieldset.tsx index 067f797f1b1b57d10baa50a0aac9f5028659f5e8..922219412998b2af2d5ce78d6badb59f13c3b653 100644 --- a/src/components/fieldset/Fieldset.tsx +++ b/src/components/fieldset/Fieldset.tsx @@ -5,14 +5,15 @@ interface FieldsetProps { legend?: React.ReactNode; } -export default function Fieldset({ +export const Fieldset: React.FC<FieldsetProps> = ({ children, legend, -}: FieldsetProps): JSX.Element { +}: FieldsetProps) => { return ( <fieldset> <legend>{legend}</legend> {children} </fieldset> ); -} +}; +export default Fieldset; \ No newline at end of file diff --git a/src/components/input/Input.test.tsx b/src/components/input/Input.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5f08dd35110154094093e36f9cb3696928731d1c --- /dev/null +++ b/src/components/input/Input.test.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Input from './Input'; + +const changeHandler = jest.fn(); + +describe('<Input>', () => { + it(`has a proper label for a11y`, () => { + const { getByRole } = render(<Input id="id" name="name" label="Name" />); + // This searches by a11y "name", labels provide names to inputs. + const input = getByRole('textbox', { name: 'Name' }); + expect(input).toBeInTheDocument(); + }); + + it(`can be set to value when uncontroller`, () => { + const { getByRole } = render(<Input id="id" name="name" label="Name" />); + const input = getByRole('textbox', { name: 'Name' }); + userEvent.type(input, 'Hello world!'); + expect(input).toHaveValue('Hello world!'); + }); + + it(`can be set to value when controller`, () => { + const { getByRole } = render( + <Input + id="id" + name="name" + label="Name" + value="" + onChange={changeHandler} + />, + ); + const input = getByRole('textbox', { name: 'Name' }); + userEvent.type(input, 'Hello world!'); + expect(changeHandler).toHaveBeenCalledTimes(12); + }); + + it(`can receive some extra classes`, () => { + const { getByRole } = render( + <Input classNames={['bad', 'robot']} id="id" name="name" label="Name" />, + ); + const input = getByRole('textbox', { name: 'Name' }); + expect(input.parentElement).toHaveClass('bad'); + expect(input.parentElement).toHaveClass('robot'); + }); + + it(`can be a required field`, () => { + const { getByRole } = render( + <Input required id="id" name="name" label="Name" />, + ); + const input = getByRole('textbox', { name: 'Name' }); + expect(input.parentElement).toHaveClass('mandatory'); + expect(input).toHaveAttribute('required'); + }); + + it(`can be a disabled field`, () => { + const { getByRole } = render( + <Input disabled id="id" name="name" label="Name" />, + ); + const input = getByRole('textbox', { name: 'Name' }); + expect(input).toHaveAttribute('disabled'); + userEvent.type(input, "Hello world!"); + expect(input).toHaveValue(''); + }); + it.each([ + ['textarea', 'textbox'], + ['email', 'textbox'], + ['number', 'spinbutton'], + ['checkbox', 'checkbox'], + ])(`can be another type than text`, (type, role) => { + const { getByRole } = render( + <Input type={type} id="id" name="name" label="Name" />, + ); + const input = getByRole(role, { name: 'Name' }); + expect(input).toBeInTheDocument(); + }); +}); diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx index dae027c7b7a1839ad151a9a3c850814a7b41f9de..875665d5d45615f7d6ec90a730ed93056cc3fece 100644 --- a/src/components/input/Input.tsx +++ b/src/components/input/Input.tsx @@ -4,33 +4,29 @@ interface InputProps { id: string; name?: string; label: React.ReactNode; - classes?: string[]; + classNames?: string[]; value?: string | undefined; required?: boolean; type?: string; + disabled?: boolean; onChange?: undefined | ReactEventHandler; - ref?: React.RefObject<unknown>; } -export default function Input({ +export const Input: React.FC<InputProps> = ({ id, - name, label, - classes = [], - // Applying this binds value to input.value, making it a controlled component - // You must also supply onChange in this case, or the field will be read-only - value = undefined, - required = false, + classNames = [], type = 'text', + required, onChange = undefined, - ref = undefined -}: InputProps): JSX.Element { - classes = ['field', ...classes]; - if (required) classes.push('mandatory'); + ...args +}: InputProps) => { + classNames = ['field', ...classNames]; + if (required) classNames.push('mandatory'); - const attributes = { id, name, type, onChange, value, ref}; + const attributes = { id, type, onChange, required, ...args }; return ( - <div className={classes.join(' ')}> + <div className={classNames.join(' ')}> <label htmlFor={id}>{label}</label> {type == 'textarea' ? ( <textarea {...attributes}></textarea> @@ -39,4 +35,5 @@ export default function Input({ )} </div> ); -} +}; +export default Input; diff --git a/src/components/locale-switcher/LocaleSwitcher.test.tsx b/src/components/locale-switcher/LocaleSwitcher.test.tsx index a671108f30d78ffdb51c8e3e3825b3b1da7cc4d1..b4cf024bab4f603aaed3014b49cdb46ff2768fe3 100644 --- a/src/components/locale-switcher/LocaleSwitcher.test.tsx +++ b/src/components/locale-switcher/LocaleSwitcher.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { screen, render, fireEvent } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import LocaleSwitcher from './LocaleSwitcher'; import i18next, { Resource } from 'i18next'; diff --git a/src/components/locale-switcher/LocaleSwitcher.tsx b/src/components/locale-switcher/LocaleSwitcher.tsx index 6d04c23b2ea95d8f06dc0eefb08e5d2bfe01a822..c3e8cdcc922c4b96301d3b789cd62428da97921d 100644 --- a/src/components/locale-switcher/LocaleSwitcher.tsx +++ b/src/components/locale-switcher/LocaleSwitcher.tsx @@ -1,20 +1,25 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -function LocaleSwitcher() { - const { t, i18n } = useTranslation(); +export const LocaleSwitcher: React.FC = () => { + const { t, i18n } = useTranslation(); return ( <select value={i18n.language} onChange={(e) => i18n.changeLanguage(e.target.value)} > - { - Object.entries(i18n.options.resources!).map(([localeCode, _]) => - <option key={localeCode} value={localeCode}>{t("language", {lng: localeCode})}</option> - ) + { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Object.entries(i18n.options.resources!).map(([localeCode, _]) => ( + <option key={localeCode} value={localeCode}> + {t('language', { lng: localeCode })} + </option> + )) } - <option key="dev" value="dev">localisation keys (dev)</option> + <option key="dev" value="dev"> + localisation keys (dev) + </option> </select> ); -} +}; export default LocaleSwitcher; diff --git a/src/components/modal/Modal.test.tsx b/src/components/modal/Modal.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..96dceb149cbc254486edd500d4efae7ac3ff3f65 --- /dev/null +++ b/src/components/modal/Modal.test.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Modal from './Modal'; + +const mockOkayClick = jest.fn(); +const mockDefaultAction = jest.fn(); +const closeCallback = jest.fn(); + +jest.unmock('./Modal'); + +describe('<Modal ..>', () => { + it.each([ + ['esc', closeCallback], + ['enter', mockDefaultAction], + ])('the Modal can call callbacks for key %s', (key, callback) => { + render( + <Modal + header="Coffee" + defaultActionCallback={mockDefaultAction} + closeCallback={closeCallback} + buttons={<></>} + > + <strong>double shot</strong> + </Modal>, + ); + const body = document.getElementsByTagName('body')[0]; + userEvent.type(body, `{${key}}`); + expect(callback).toBeCalled(); + }); + + test(`The modal renders buttons`, () => { + const { getByRole } = render( + <Modal + header="Coffee" + buttons={<button onClick={mockOkayClick}>Make coffee</button>} + > + <strong>double shot</strong> + </Modal>, + ); + expect(getByRole('button', { name: /make coffee/i })).toBeInTheDocument(); + }); + + test(`modal default options`, () => { + const { getByRole } = render( + <Modal + header="Coffee" + buttons={<></>} + curtain={true} + closeButton={true} + closeCallback={closeCallback} + > + <strong>double shot</strong> + </Modal>, + ); + const wrapper = document.getElementsByClassName('modal-wrapper')[0]; + expect(wrapper).toHaveClass('curtain'); + userEvent.click(getByRole('button', { name: 'close' })); + expect(closeCallback).toBeCalled(); + }); + + test(`modal non-default options`, () => { + const { queryByRole } = render( + <Modal + header="Coffee" + buttons={<></>} + curtain={false} + closeButton={false} + closeCallback={closeCallback} + > + <strong>double shot</strong> + </Modal>, + ); + const wrapper = document.getElementsByClassName('modal-wrapper')[0]; + expect(wrapper).not.toHaveClass('curtain'); + expect(queryByRole('button', { name: 'close' })).not.toBeInTheDocument(); + }); + + it(`Get an error when passed a bad dom node selector`, () => { + const { getByText } = render( + <Modal header="Coffee" DOMNodeSelector=".badselector" buttons={<></>}> + <strong>double shot</strong> + </Modal>, + ); + expect(getByText(/did not yield a dom node/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/modal/Modal.tsx b/src/components/modal/Modal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b7fb6c384b7f1620f158d34ff9599626fe71251e --- /dev/null +++ b/src/components/modal/Modal.tsx @@ -0,0 +1,85 @@ +import React, { useEffect } from 'react'; +import ReactDOM from 'react-dom'; + +export interface ModalProps { + DOMNodeSelector?: string; + curtain?: boolean; + children: React.ReactNode; + header: React.ReactNode; + defaultActionCallback?: CallableFunction | undefined; + closeCallback?: CallableFunction | undefined; + closeButton?: boolean; + buttons: React.ReactNode; +} + +export const Modal: React.FC<ModalProps> = ({ + children, + closeCallback, + defaultActionCallback, + header, + buttons, + DOMNodeSelector = 'body', + curtain = true, + closeButton = true, +}: ModalProps) => { + const wrapper = document.createElement('div'); + wrapper.classList.add('modal-wrapper'); + if (curtain) wrapper.classList.add('curtain'); + const portal = document.querySelector(DOMNodeSelector); + if (!portal) + return ( + <strong> + Querying "{DOMNodeSelector}" did not yield a DOM node + </strong> + ); + + // Mount and unmount the modal in the portal + useEffect(() => { + portal.appendChild(wrapper); + portal.classList.add('modal-active'); + return () => { + portal.removeChild(wrapper); + portal.classList.remove('modal-active'); + }; + }, []); + + const keyUpHandler = (event: KeyboardEvent) => { + if (event.key == 'Escape' && typeof closeCallback == 'function') + closeCallback(); + if (event.key == 'Enter' && typeof defaultActionCallback == 'function') + defaultActionCallback(); + }; + + useEffect(() => { + window.addEventListener('keyup', keyUpHandler); + return () => { + window.removeEventListener('keyup', keyUpHandler); + }; + }); + + const randomEnough = (+new Date()).toString(36).slice(-10); + + const close = () => typeof closeCallback == 'function' && closeCallback(); + return ReactDOM.createPortal( + <div + className="modal" + role="dialog" + aria-labelledby={`${randomEnough}-header`} + aria-describedby={`${randomEnough}-main`} + > + <header id={`${randomEnough}-header`}> + <h5>{header}</h5> + {closeButton && ( + <button className="close" onClick={close} aria-label="close"> + ⨯ + </button> + )} + </header> + <main id={`${randomEnough}-main`}>{children}</main> + <footer>{buttons}</footer> + </div>, + wrapper, + ); +}; + +export default Modal; diff --git a/src/components/problem-dropdown/ProblemDropdown.tsx b/src/components/problem-dropdown/ProblemDropdown.tsx index 79d1e7ea873466af50ec40c065c3f73ebb53605e..5fe2ecb0ce8b003d4ff73e6edfcaa25356a39cd6 100644 --- a/src/components/problem-dropdown/ProblemDropdown.tsx +++ b/src/components/problem-dropdown/ProblemDropdown.tsx @@ -6,9 +6,11 @@ export interface ProblemDropdownProps extends GenericProblemDropdownProps { problems: Record<string, Problem>; } -export function ProblemDropdown(props: ProblemDropdownProps): JSX.Element { +export const ProblemDropdown: React.FC<ProblemDropdownProps> = ( + props: ProblemDropdownProps, +) => { return <GenericProblemDropdown {...props} />; -} +}; export interface ProblemGroupDropdownProps { id: string; @@ -19,14 +21,14 @@ export interface ProblemGroupDropdownProps { problemGroups: Record<string, ProblemGroup>; } -export function ProblemGroupDropdown({ +export const ProblemGroupDropdown: React.FC<ProblemGroupDropdownProps> = ({ id, intro, selectProblemGroup, problemGroup, setProblemGroup, problemGroups, -}: ProblemGroupDropdownProps): JSX.Element { +}: ProblemGroupDropdownProps) => { return ( <GenericProblemDropdown id={id} @@ -37,7 +39,7 @@ export function ProblemGroupDropdown({ setProblem={setProblemGroup} /> ); -} +}; export interface GenericProblemDropdownProps { id: string; @@ -48,14 +50,14 @@ export interface GenericProblemDropdownProps { setProblem: CallableFunction; } -function GenericProblemDropdown({ +export const GenericProblemDropdown: React.FC<GenericProblemDropdownProps> = ({ id, intro, selectProblem, problems, problem, setProblem, -}: GenericProblemDropdownProps) { +}: GenericProblemDropdownProps) => { return ( <div className="field"> <label htmlFor={id}>{intro}</label> @@ -75,4 +77,4 @@ function GenericProblemDropdown({ </select> </div> ); -} +}; diff --git a/src/components/spinner/Spinner.tsx b/src/components/spinner/Spinner.tsx index 6bff6b33d6a4945ecceab63b04c3719874f09d7b..b481f3044534ffcbabcb5f93732ab1e804f1f1d5 100644 --- a/src/components/spinner/Spinner.tsx +++ b/src/components/spinner/Spinner.tsx @@ -5,7 +5,10 @@ export interface SpinnerProps { loading?: boolean; } -function Spinner({ big = false, loading = true }: SpinnerProps): JSX.Element { +export const Spinner: React.FC<SpinnerProps> = ({ + big = false, + loading = true, +}: SpinnerProps) => { if (!loading) return <div className="spinner"></div>; const classes = ['spinner', 'loading']; if (big) classes.push('big'); @@ -16,6 +19,6 @@ function Spinner({ big = false, loading = true }: SpinnerProps): JSX.Element { aria-label="Loading" ></div> ); -} +}; export default Spinner; diff --git a/src/components/suggestion-list/ProblemSuggestionList.tsx b/src/components/suggestion-list/ProblemSuggestionList.tsx index 2cc6b93f583204e2e26e3d3578d5844f1c57ba61..4da00ba10b67a83e1500091f08dc14cfa7f30496 100644 --- a/src/components/suggestion-list/ProblemSuggestionList.tsx +++ b/src/components/suggestion-list/ProblemSuggestionList.tsx @@ -3,28 +3,28 @@ import React from 'react'; import type { Suggestion } from 'src/types'; export interface ProblemSuggestionListProps { - intro: string, - suggestions: Suggestion[] + intro: string; + suggestions: Suggestion[]; } -function ProblemSuggestionList({ +export const ProblemSuggestionList: React.FC<ProblemSuggestionListProps> = ({ intro, suggestions, -}: ProblemSuggestionListProps) { +}: ProblemSuggestionListProps) => { return ( <> - <p><Markdown>{intro}</Markdown></p> + <p> + <Markdown>{intro}</Markdown> + </p> <ul> {suggestions.map((suggestion: Suggestion, i: number) => ( <li key={i}> - <a href={suggestion.link}> - {suggestion.description} - </a> + <a href={suggestion.link}>{suggestion.description}</a> </li> ))} </ul> </> ); -} +}; export default ProblemSuggestionList; diff --git a/src/components/urgent-request/UrgentRequest.module.scss b/src/components/urgent-request/UrgentRequest.module.scss index b0ae6d28841e3b09fee41df30e596d7a3f53abfe..4c4ce9b50b02724c9934132c19aa93af353a64e2 100644 --- a/src/components/urgent-request/UrgentRequest.module.scss +++ b/src/components/urgent-request/UrgentRequest.module.scss @@ -1,3 +1,3 @@ .UrgentRequest { - border: 1px rgb(113, 0, 10) solid; + border: 2px rgb(113, 0, 10) solid; } diff --git a/src/components/urgent-request/UrgentRequest.test.tsx b/src/components/urgent-request/UrgentRequest.test.tsx index 422a9c95da902fea331b0e1ac5b33799b9dc7220..456f4aae43fe4fa140cacd6f2c151602a278e370 100644 --- a/src/components/urgent-request/UrgentRequest.test.tsx +++ b/src/components/urgent-request/UrgentRequest.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import UrgentRequest from './UrgentRequest'; import userEvent from '@testing-library/user-event'; -import styles from './UrgentRequest.module.scss'; const mockSendUrgent = jest.fn(); diff --git a/src/components/urgent-request/UrgentRequest.tsx b/src/components/urgent-request/UrgentRequest.tsx index 5e373fcd9821f8724f19a4977a10fdbe297af440..aca85881014483056bb1259e6f53b351ea8fc8a6 100644 --- a/src/components/urgent-request/UrgentRequest.tsx +++ b/src/components/urgent-request/UrgentRequest.tsx @@ -7,7 +7,9 @@ export interface UrgentRequestProps { sendUrgent: CallableFunction; } -function UrgentRequest({ sendUrgent }: UrgentRequestProps): JSX.Element { +export const UrgentRequest: React.FC<UrgentRequestProps> = ({ + sendUrgent, +}: UrgentRequestProps) => { const [urgent, setUrgent] = useState<boolean>(false); const [urgentText, setUrgentText] = useState<string>(''); const { t } = useTranslation(); @@ -43,6 +45,6 @@ function UrgentRequest({ sendUrgent }: UrgentRequestProps): JSX.Element { </button> </fieldset> ); -} +}; export default UrgentRequest; diff --git a/src/tests/App.test.tsx b/src/tests/App.test.tsx index aea792a8b0192c4c3529e382c577f93dd3e26999..ea3c3c853375d87dfc1b86afda8812030661eecc 100644 --- a/src/tests/App.test.tsx +++ b/src/tests/App.test.tsx @@ -1,10 +1,12 @@ import * as React from 'react'; -import { act, render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import App from '../App'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import config from '../config.yml'; import userEvent from '@testing-library/user-event'; -import urgencyStyles from '../components/urgent-request/UrgentRequest.scss'; +import { config as transitionConfig } from 'react-transition-group'; +// Disable CSSTransitions to prevent them from messing with test conditions +transitionConfig.disabled = true; // Use a fixture instead of the real config file. jest.mock('../config.yml', () => require('../__mocks__/config.json')); @@ -80,6 +82,15 @@ describe('<App> with mocked config', () => { name: 'How would you best describe your technical problem?', }); + // For technical issues, an urgent request can be made. + const urgencyCheckbox = getByRole('checkbox', { + name: /Urgent request/, + }); + expect(urgencyCheckbox).toBeInTheDocument(); + + // Assert that the urgency form is still hidden + expect(queryByText(/Confirm urgent request/i)).not.toBeInTheDocument(); + // We tell the form we have a hosting problem userEvent.selectOptions(problemInput, ['hosting']); @@ -91,17 +102,7 @@ describe('<App> with mocked config', () => { expect(getByText(/Recyling the gremlin states/i)).toBeInTheDocument(); expect(getByText(/Generic technical problems fix/i)).toBeInTheDocument(); - // For technical issues, an urgent request can be made. - const urgencyCheckbox = getByRole('checkbox', { - name: /Urgent request/, - }); - expect(urgencyCheckbox).toBeInTheDocument(); - - // Assert that the urgency form is still hidden - expect(queryByText(/Confirm urgent request/i)).not.toBeInTheDocument(); - userEvent.click(urgencyCheckbox); - // Assert that the urgency form is now visible expect(getByText(/Confirm urgent request/i)).toBeInTheDocument();