From afba24a22f8803c10884ce259d18a60b013ee5e3 Mon Sep 17 00:00:00 2001 From: Chris <chris@chrissnijder.nl> Date: Thu, 18 Feb 2021 02:32:42 +0100 Subject: [PATCH] Too much to begin to list.. --- package.json | 4 +- pnpm-lock.yaml | 61 +++++++- src/App.module.scss | 3 + src/App.test.tsx | 14 -- src/App.tsx | 103 +++++++++----- src/__mocks__/config.json | 56 ++++---- .../contact-form/ContactForm.module.scss | 3 + .../contact-form/ContactForm.test.tsx | 41 ++++++ src/components/contact-form/ContactForm.tsx | 134 ++++++++++++++++++ src/components/fieldset/Fieldset.test.tsx | 20 +++ src/components/fieldset/Fieldset.tsx | 18 +++ src/components/input/Input.tsx | 42 ++++++ .../problem-dropdown/ProblemDropdown.test.tsx | 16 +-- .../problem-dropdown/ProblemDropdown.tsx | 65 +++++---- src/components/spinner/Spinner.test.tsx | 38 +++-- src/components/spinner/Spinner.tsx | 2 +- .../urgent-request/UrgentRequest.module.scss | 13 -- .../urgent-request/UrgentRequest.test.tsx | 24 +--- .../urgent-request/UrgentRequest.tsx | 58 +++----- src/config.yml | 84 ++++++----- src/styles/transitions/panel.module.scss | 27 ++++ src/tests/App.test.tsx | 127 +++++++++++++++++ src/types.ts | 6 +- 23 files changed, 719 insertions(+), 240 deletions(-) create mode 100644 src/App.module.scss delete mode 100644 src/App.test.tsx create mode 100644 src/components/contact-form/ContactForm.module.scss create mode 100644 src/components/contact-form/ContactForm.test.tsx create mode 100644 src/components/contact-form/ContactForm.tsx create mode 100644 src/components/fieldset/Fieldset.test.tsx create mode 100644 src/components/fieldset/Fieldset.tsx create mode 100644 src/components/input/Input.tsx create mode 100644 src/styles/transitions/panel.module.scss create mode 100644 src/tests/App.test.tsx diff --git a/package.json b/package.json index 298b1a6..e590c60 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "license": "Apache-2.0", "dependencies": { "i18next": "^19.8.7", - "identity-obj-proxy": "^3.0.0", "js-yaml": "^4.0.0", "react": "^17.0.0", "react-dom": "^17.0.0", @@ -39,6 +38,7 @@ "@types/jest": "^26.0.20", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", + "@types/react-transition-group": "^4.4.0", "@types/snowpack-env": "^2.3.2", "@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/parser": "^4.15.0", @@ -47,9 +47,11 @@ "eslint-plugin-jest-dom": "^3.6.5", "eslint-plugin-react": "^7.22.0", "eslint-plugin-testing-library": "^3.10.1", + "identity-obj-proxy": "^3.0.0", "jest-cli": "^26.6.3", "markdown-to-jsx": "^7.1.1", "prettier": "^2.0.5", + "react-transition-group": "^4.4.1", "rxjs": "^6.6.3", "sass": "^1.32.6", "snowpack": "^3.0.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f441de..7caa641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,12 @@ devDependencies: '@snowpack/plugin-typescript': 1.2.1_typescript@4.1.3 '@testing-library/jest-dom': 5.11.9 '@testing-library/react': 11.2.5_react-dom@17.0.1+react@17.0.1 - '@testing-library/react-hooks': 5.0.3_react-dom@17.0.1+react@17.0.1 + '@testing-library/react-hooks': 5.0.3_884e1b78f106e7ec36cdc9eac308ff33 '@testing-library/user-event': 12.6.3 '@types/jest': 26.0.20 '@types/react': 17.0.1 '@types/react-dom': 17.0.0 + '@types/react-transition-group': 4.4.0 '@types/snowpack-env': 2.3.3 '@typescript-eslint/eslint-plugin': 4.15.0_97ae0b5fdeb2dd2a56389613b9d78781 '@typescript-eslint/parser': 4.15.0_eslint@7.20.0+typescript@4.1.3 @@ -31,6 +32,8 @@ devDependencies: jest-cli: 26.6.3_ts-node@9.1.1 markdown-to-jsx: 7.1.1_react@17.0.1 prettier: 2.2.1 + react-test-renderer: 17.0.1_react@17.0.1 + react-transition-group: 4.4.1_react-dom@17.0.1+react@17.0.1 rxjs: 6.6.3 sass: 1.32.6 snowpack: 3.0.11 @@ -2576,7 +2579,7 @@ packages: yarn: '>=1' resolution: integrity: sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ== - /@testing-library/react-hooks/5.0.3_react-dom@17.0.1+react@17.0.1: + /@testing-library/react-hooks/5.0.3_884e1b78f106e7ec36cdc9eac308ff33: dependencies: '@babel/runtime': 7.12.13 '@types/react': 17.0.2 @@ -2586,6 +2589,7 @@ packages: react: 17.0.1 react-dom: 17.0.1_react@17.0.1 react-error-boundary: 3.1.0_react@17.0.1 + react-test-renderer: 17.0.1_react@17.0.1 dev: true peerDependencies: react: '>=16.9.0' @@ -2727,6 +2731,12 @@ packages: dev: true resolution: integrity: sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== + /@types/react-transition-group/4.4.0: + dependencies: + '@types/react': 17.0.2 + dev: true + resolution: + integrity: sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== /@types/react/17.0.1: dependencies: '@types/prop-types': 15.7.3 @@ -3813,6 +3823,13 @@ packages: dev: true resolution: integrity: sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== + /dom-helpers/5.2.0: + dependencies: + '@babel/runtime': 7.12.13 + csstype: 3.0.6 + dev: true + resolution: + integrity: sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== /domexception/2.0.1: dependencies: webidl-conversions: 5.0.0 @@ -6401,6 +6418,42 @@ packages: node: '>=0.10.0' resolution: integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== + /react-shallow-renderer/16.14.1_react@17.0.1: + dependencies: + object-assign: 4.1.1 + react: 17.0.1 + react-is: 17.0.1 + dev: true + peerDependencies: + react: ^16.0.0 || ^17.0.0 + resolution: + integrity: sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg== + /react-test-renderer/17.0.1_react@17.0.1: + dependencies: + object-assign: 4.1.1 + react: 17.0.1 + react-is: 17.0.1 + react-shallow-renderer: 16.14.1_react@17.0.1 + scheduler: 0.20.1 + dev: true + peerDependencies: + react: 17.0.1 + resolution: + integrity: sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA== + /react-transition-group/4.4.1_react-dom@17.0.1+react@17.0.1: + dependencies: + '@babel/runtime': 7.12.13 + dom-helpers: 5.2.0 + loose-envify: 1.4.0 + prop-types: 15.7.2 + react: 17.0.1 + react-dom: 17.0.1_react@17.0.1 + dev: true + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + resolution: + integrity: sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== /react/17.0.1: dependencies: loose-envify: 1.4.0 @@ -6752,7 +6805,6 @@ packages: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 - dev: false resolution: integrity: sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw== /semver/5.7.1: @@ -7650,6 +7702,7 @@ specifiers: '@types/jest': ^26.0.20 '@types/react': ^17.0.0 '@types/react-dom': ^17.0.0 + '@types/react-transition-group': ^4.4.0 '@types/snowpack-env': ^2.3.2 '@typescript-eslint/eslint-plugin': ^4.15.0 '@typescript-eslint/parser': ^4.15.0 @@ -7667,6 +7720,8 @@ specifiers: react: ^17.0.0 react-dom: ^17.0.0 react-i18next: ^11.8.5 + react-test-renderer: ^17.0.1 + react-transition-group: ^4.4.1 rxjs: ^6.6.3 sass: ^1.32.6 snowpack: ^3.0.11 diff --git a/src/App.module.scss b/src/App.module.scss new file mode 100644 index 0000000..9d7ba47 --- /dev/null +++ b/src/App.module.scss @@ -0,0 +1,3 @@ +.troubleshooting { + margin-bottom: 4rem; +} \ No newline at end of file diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index c018e32..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; -import App from './App'; -import config from "./config.yml"; - -jest.mock("./config.yml", ()=>require("./__mocks__/config.json" ) ); - -describe('<App>', () => { - it('renders with mocked config', () => { - const { getByText } = render(<App />); - const title = getByText("Report a problem"); - expect(title).toBeDefined(); - }); -}); diff --git a/src/App.tsx b/src/App.tsx index a069b20..58ac19a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,15 +6,14 @@ import { ProblemGroupDropdown, } from './components/problem-dropdown/ProblemDropdown'; import ProblemSuggestionList from './components/suggestion-list/ProblemSuggestionList'; -import UrgentRequest from './components/urgent-request/UrgentRequest'; import config from './config.yml'; -import Markdown from "markdown-to-jsx"; +import Markdown from 'markdown-to-jsx'; import { useLanguage } from './hooks'; +import type { Problem, Suggestion } from './types'; +import ContactForm from './components/contact-form/ContactForm'; +import styles from './App.module.scss'; -interface AppProps { -} - -function App({}: AppProps) { +function App(): JSX.Element { const { t } = useTranslation(); const [problemGroup, _setProblemGroup] = useState<string>(''); const [problem, setProblem] = useState<string>(''); @@ -22,30 +21,59 @@ function App({}: AppProps) { const localized = config.localisations[language == 'dev' ? 'en' : language].translation; const problemGroups = localized.problemGroups; - let problems = []; - if (problemGroup) problems = localized.problemGroups[problemGroup].problems; - let suggestions = []; - if (problemGroup && problem) - suggestions = - localized.problemGroups[problemGroup].problems[problem].suggestions; + let problems: Record<string, Problem> = {}; + let suggestions: Suggestion[] = []; + let problemDescribe = ''; + let suggestionDescription = ''; + + // A problemgroup was selected? + if (problemGroup) { + // Does the problem group have problems? + if (localized.problemGroups[problemGroup].problems) { + problems = localized.problemGroups[problemGroup].problems; + problemDescribe = localized.problemGroups[problemGroup].problemDescribe; + } + // Does the problem group have suggestions? + if (localized.problemGroups[problemGroup].suggestions) + suggestions = localized.problemGroups[problemGroup].suggestions; + suggestionDescription = + localized.problemGroups[problemGroup].suggestionDescription; + // If a problem is selected, and the problem has suggestions.. + if ( + problem && + localized.problemGroups[problemGroup].problems[problem].suggestions + ) { + // Add the suggestions to the suggestions array + suggestions = suggestions.concat( + localized.problemGroups[problemGroup].problems[problem].suggestions, + ); + suggestionDescription = + localized.problemGroups[problemGroup].problems[problem] + .suggestionDescription; + } + } function setProblemGroup(newGroup: string) { setProblem(''); _setProblemGroup(newGroup); } - function sendUrgent() {alert("SENT!")} - return ( <Suspense fallback="Loading..."> <LocaleSwitcher /> <header> <img src="" /> - <h1><Markdown>{t('title')}</Markdown></h1> - <p><Markdown>{t('subtitle')}</Markdown></p> + <h1> + <Markdown>{t('title')}</Markdown> + </h1> + <p> + <Markdown>{t('subtitle')}</Markdown> + </p> </header> - <form> + <form className={styles.troubleshooting}> <ProblemGroupDropdown + id="problemGroup" + intro={localized.problemDescribe} selectProblemGroup={t('selectProblemGroup')} problemGroup={problemGroup} setProblemGroup={setProblemGroup} @@ -53,29 +81,40 @@ function App({}: AppProps) { /> {problemGroup && ( <> - <p>{t(`problemGroups.${problemGroup}.problemDescribe`)}</p> - <ProblemDropdown - selectProblem={t('selectProblem')} - problem={problem} - setProblem={setProblem} - problems={problems} - /> - {problem && ( + {Object.keys(problems).length > 0 && ( + <> + <ProblemDropdown + id="problem" + intro={problemDescribe} + selectProblem={t('selectProblem')} + problem={problem} + setProblem={setProblem} + problems={problems} + /> + </> + )} + {suggestions.length > 0 && ( <ProblemSuggestionList - intro={t( - `problemGroups.${problemGroup}.problems.${problem}.suggestionDescription`, - )} + intro={suggestionDescription} suggestions={suggestions} /> )} </> )} </form> - {problemGroup && problem && problemGroup == 'technical' && ( - <UrgentRequest sendUrgent={sendUrgent} /> - )} + <h1> + <Markdown>{t('contact.title')}</Markdown> + </h1> + <ContactForm {...{ problem, problemGroup }} /> + <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> + <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> + <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> + <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> + <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> + <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> + </Suspense> ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/__mocks__/config.json b/src/__mocks__/config.json index dc40c09..755c9a6 100644 --- a/src/__mocks__/config.json +++ b/src/__mocks__/config.json @@ -4,21 +4,40 @@ "en": { "translation": { "language": "English", - "title": "Report a problem", + "title": "Report a problem (mocked)", "subtitle": "Let's troubleshoot together. What type of issue are you experiencing?", "selectProblemGroup": "Select an issue type..", "selectProblem": "Select issue..", + "problemDescribe": "The nature of my problem is:", + "submit": "Submit request", "urgency": { "legend": "Confirm urgent request", "foldText": "Urgent request?", "intro": "__CAUTION__: by selecting this, your request will escalate to highest priority and immediately alert on-call operators, 24/7.\n\nYou will be billed {{ urgencyFee }} for this type of request, unless we find that the urgency of your issue was justified through risk of immediate and irreparable damage.\n\nIf you are okay with this, type: `URGENT` below and then click on \"Submit Urgent Request\"", "submit": "Submit Urgent Request", - "urgent": "URGENT" + "urgent": "URGENT", + "typeUrgentHere": "Type URGENT in capital letters here to unlock the urgent submit button." + }, + "contact": { + "title": "Tell us about your problem", + "name": "Name:", + "phone": "Phone number:", + "email": "E-mail address:", + "message": "Your description of the problem:", + "tell-us": "What do you want to tell us?", + "details": "Contact details" }, "problemGroups": { "technical": { "description": "Technical", - "problemDescribe": "How would you best describe your adminstrative question?", + "problemDescribe": "How would you best describe your technical problem?", + "suggestionDescription": "Here are some solutions to some of the most common technical issues:", + "suggestions": [ + { + "link": "https://greenhost.net/helpdesk/website/hosting-management/#maintenance", + "description": "Generic technical problems fix" + } + ], "problems": { "hosting": { "description": "My website is not reachable or is not working properly", @@ -38,29 +57,18 @@ }, "administrative": { "description": "Administrative question", - "problemDescribe": "How would you best describe your adminstrative question?", - "problems": { - "invoicing": { - "description": "I have a question regarding an invoice", - "suggestionDescription": "Here are some of the most common examples of invoice questions:", - "suggestions": [ - { - "link": "https://greenhost.net/contact/#payment-details", - "description": "I need Greenhost's payment information." - } - ] + "problemDescribe": "How would you best describe your adminstrative problem?", + "suggestionDescription": "Here are some of the most common examples of administrative questions:", + "suggestions": [ + { + "link": "https://greenhost.net/contact/#payment-details", + "description": "I need Greenhost's payment information." }, - "legal": { - "description": "I have a legal question", - "suggestionDescription": "Here are some of the most common examples of legal questions:", - "suggestions": [ - { - "link": "https://greenhost.net/legal/terms-and-conditions/", - "description": "Where can I find your terms and conditions?" - } - ] + { + "link": "https://greenhost.net/legal/terms-and-conditions/", + "description": "Where can I find your terms and conditions?" } - } + ] } } } diff --git a/src/components/contact-form/ContactForm.module.scss b/src/components/contact-form/ContactForm.module.scss new file mode 100644 index 0000000..7ef3113 --- /dev/null +++ b/src/components/contact-form/ContactForm.module.scss @@ -0,0 +1,3 @@ +.ContactForm fieldset { + margin: 1rem 0 1rem 0; +} diff --git a/src/components/contact-form/ContactForm.test.tsx b/src/components/contact-form/ContactForm.test.tsx new file mode 100644 index 0000000..87e93f6 --- /dev/null +++ b/src/components/contact-form/ContactForm.test.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import ContactForm from './ContactForm'; +import userEvent from '@testing-library/user-event'; + +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 { 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(); + 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 + */ + + }); +}); diff --git a/src/components/contact-form/ContactForm.tsx b/src/components/contact-form/ContactForm.tsx new file mode 100644 index 0000000..d58b01b --- /dev/null +++ b/src/components/contact-form/ContactForm.tsx @@ -0,0 +1,134 @@ +import Markdown from 'markdown-to-jsx'; +import React, { ChangeEvent, FormEvent, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import UrgentRequest from '../urgent-request/UrgentRequest'; +import { CSSTransition, SwitchTransition } from 'react-transition-group'; +import styles from './ContactForm.module.scss'; +import panelTransition from '../../styles/transitions/panel.module.scss'; +import Fieldset from '../fieldset/Fieldset'; +import Input from '../input/Input'; + +export interface ContactFormProps { + problemGroup: string; + problem: string; +} + +function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element { + const [urgentIntent, setUrgentIntent] = useState<boolean>(false); + const [state, setState] = useState<Record<string, string>>({}); + const { t } = useTranslation(); + + const changeHandler = ( + event: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>, + ) => { + setState({ ...state, ...{ [event.target.name]: event.target.value } }); + }; + + const changeUrgentIntent = (event: ChangeEvent<HTMLInputElement>) => { + setUrgentIntent(event.target.checked); + }; + + function submit(event: FormEvent<HTMLFormElement>) { + event.preventDefault(); + sendRequest(); + } + + const sendRequest = (urgent = false) => { + const formData = { ...state, ...{ urgent: urgent ? 'yes' : 'no' } }; + + Object.fromEntries( + Object.entries(formData).map(([name, value]) => [ + `SupportForm[${name}]`, + value, + ]), + ), + console.log(formData); + }; + + return ( + <form onSubmit={submit} className={`${styles.ContactForm}`}> + + <Fieldset legend={t('contact.details')}> + <Input + label={t('contact.name')} + id="name" + name="name" + required + onChange={changeHandler} + /> + <Input + label={t('contact.phone')} + id="phone" + name="phone" + onChange={changeHandler} + /> + <Input + label={t('contact.identification')} + id="identification" + name="identification" + onChange={changeHandler} + required + /> + <Input + label={t('contact.email')} + id="email" + name="email" + onChange={changeHandler} + required + /> + </Fieldset> + <Fieldset legend={t('contact.tell-us')}> + <Input + label={t('Message')} + id="message" + name="message" + onChange={changeHandler} + type="textarea" + required + /> + </Fieldset> + {problemGroup == 'technical' && ( + <div className="field.switch"> + <input + id="urgency-checkbox" + type="checkbox" + className="switch fold-button" + onChange={changeUrgentIntent} + /> + <label htmlFor="urgency-checkbox">{t('urgency.foldText')}</label> + </div> + )} + <div className="field"> + <SwitchTransition> + <CSSTransition + key={urgentIntent ? 'urgent' : 'normal'} + in={urgentIntent} + classNames={{ ...panelTransition }} + unmountOnExit={true} + mountOnEnter={true} + addEndListener={(node: HTMLElement, done: EventListener) => { + // use the css transitionend event to mark the finish of a transition + node.addEventListener('transitionend', done, false); + }} + > + {urgentIntent ? ( + <UrgentRequest sendUrgent={() => sendRequest(true)} /> + ) : ( + <button + className="good" + onClick={(event) => { + event?.preventDefault(); + sendRequest(); + }} + > + {t('submit')} + </button> + )} + </CSSTransition> + </SwitchTransition> + </div> + </form> + ); +} + +export default ContactForm; diff --git a/src/components/fieldset/Fieldset.test.tsx b/src/components/fieldset/Fieldset.test.tsx new file mode 100644 index 0000000..bc176da --- /dev/null +++ b/src/components/fieldset/Fieldset.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import Fieldset from './Fieldset'; + +describe('<Fieldset>', () => { + it('renders a fieldset with a legend and children elements', () => { + const legend = () => <strong>Zelda</strong>; + + const { getByRole, getByText } = render( + <Fieldset legend={legend()}> + <blockquote>It's dangerous to go alone! take this.🗡ï¸</blockquote> + </Fieldset>, + ); + const fieldset = getByRole('group'); + expect(fieldset).toBeInTheDocument(); + expect(getByText(/zelda/i)).toBeInTheDocument(); + expect(getByText(/take this/i)).toBeInTheDocument(); + expect(getByText(/🗡ï¸/)).toBeInTheDocument(); + }); +}); diff --git a/src/components/fieldset/Fieldset.tsx b/src/components/fieldset/Fieldset.tsx new file mode 100644 index 0000000..067f797 --- /dev/null +++ b/src/components/fieldset/Fieldset.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +interface FieldsetProps { + children?: React.ReactNode; + legend?: React.ReactNode; +} + +export default function Fieldset({ + children, + legend, +}: FieldsetProps): JSX.Element { + return ( + <fieldset> + <legend>{legend}</legend> + {children} + </fieldset> + ); +} diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx new file mode 100644 index 0000000..dae027c --- /dev/null +++ b/src/components/input/Input.tsx @@ -0,0 +1,42 @@ +import React, { ReactEventHandler } from 'react'; + +interface InputProps { + id: string; + name?: string; + label: React.ReactNode; + classes?: string[]; + value?: string | undefined; + required?: boolean; + type?: string; + onChange?: undefined | ReactEventHandler; + ref?: React.RefObject<unknown>; +} +export default function Input({ + 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, + type = 'text', + onChange = undefined, + ref = undefined +}: InputProps): JSX.Element { + classes = ['field', ...classes]; + if (required) classes.push('mandatory'); + + const attributes = { id, name, type, onChange, value, ref}; + + return ( + <div className={classes.join(' ')}> + <label htmlFor={id}>{label}</label> + {type == 'textarea' ? ( + <textarea {...attributes}></textarea> + ) : ( + <input {...attributes} /> + )} + </div> + ); +} diff --git a/src/components/problem-dropdown/ProblemDropdown.test.tsx b/src/components/problem-dropdown/ProblemDropdown.test.tsx index c9e36a8..d7f958f 100644 --- a/src/components/problem-dropdown/ProblemDropdown.test.tsx +++ b/src/components/problem-dropdown/ProblemDropdown.test.tsx @@ -17,6 +17,8 @@ const selectProblem = translation.selectProblem; const mockSetProblem: CallableFunction = jest.fn(); const problemDropdownProps: ProblemDropdownProps = { + id: 'problem', + intro: problemGroups.technical.problemDescribe, problems, selectProblem, problem: '', // none selected @@ -24,6 +26,8 @@ const problemDropdownProps: ProblemDropdownProps = { }; const problemGroupDropdownProps: ProblemGroupDropdownProps = { + id: 'problemGroup', + intro: translation.problemDescribe, problemGroups, selectProblemGroup, problemGroup: '', // none selected @@ -46,26 +50,22 @@ const getComponentByProps = ( return ProblemGroupDropdown; }; - describe.each([ [ 'ProblemDropdown', // Name for decribe() 'problems', // Name of item type problemDropdownProps, // Props to render with ], - [ - 'ProblemGroupDropdown', - 'problemroups', - problemGroupDropdownProps, - ], + ['ProblemGroupDropdown', 'problemgroups', problemGroupDropdownProps], ])('<%s>', (_, type, props) => { const Component = getComponentByProps(props); const firstProblem = getFirstItem(props); it(`renders a list of ${type}`, () => { - render(<Component {...props as any} />); + render(<Component {...(props as any)} />); // Check the default value is selected const elSelect = screen.getByRole('combobox'); - if ('selectProblem' in props) { + if ("selectProblem" in props) { + expect(screen.getByText(props.intro)).toBeInTheDocument(); expect(elSelect).toHaveDisplayValue(props.selectProblem); } else { expect(elSelect).toHaveDisplayValue(props.selectProblemGroup); diff --git a/src/components/problem-dropdown/ProblemDropdown.tsx b/src/components/problem-dropdown/ProblemDropdown.tsx index 34089d8..79d1e7e 100644 --- a/src/components/problem-dropdown/ProblemDropdown.tsx +++ b/src/components/problem-dropdown/ProblemDropdown.tsx @@ -1,62 +1,69 @@ import React from 'react'; +import type { Problem, ProblemGroup } from 'src/types'; -export function ProblemDropdown({ - selectProblem, - problem, - setProblem, - problems -}: ProblemDropdownProps) { +export interface ProblemDropdownProps extends GenericProblemDropdownProps { + intro: string; + problems: Record<string, Problem>; +} - return ( - <GenericProblemDropdown - problem={problem} - problems={problems} - selectProblem={selectProblem} - setProblem={setProblem} - /> - ) +export function ProblemDropdown(props: ProblemDropdownProps): JSX.Element { + return <GenericProblemDropdown {...props} />; } export interface ProblemGroupDropdownProps { + id: string; + intro: string; selectProblemGroup: string; problemGroup: string; setProblemGroup: CallableFunction; - problemGroups: object; + problemGroups: Record<string, ProblemGroup>; } export function ProblemGroupDropdown({ + id, + intro, selectProblemGroup, problemGroup, setProblemGroup, - problemGroups -}: ProblemGroupDropdownProps) { + problemGroups, +}: ProblemGroupDropdownProps): JSX.Element { return ( - <GenericProblemDropdown - problem={problemGroup} - problems={problemGroups} - selectProblem={selectProblemGroup} - setProblem={setProblemGroup} + <GenericProblemDropdown + id={id} + intro={intro} + problem={problemGroup} + problems={problemGroups} + selectProblem={selectProblemGroup} + setProblem={setProblemGroup} /> - ) + ); } - -export interface ProblemDropdownProps { +export interface GenericProblemDropdownProps { + id: string; + intro: string; selectProblem: string; problem: string; - problems: object, + problems: Record<string, Problem | ProblemGroup>; setProblem: CallableFunction; } function GenericProblemDropdown({ + id, + intro, selectProblem, problems, problem, setProblem, -}: ProblemDropdownProps) { +}: GenericProblemDropdownProps) { return ( <div className="field"> - <select value={problem} onChange={(e) => setProblem(e.target.value)}> + <label htmlFor={id}>{intro}</label> + <select + id={id} + value={problem} + onChange={(e) => setProblem(e.target.value)} + > <option key="_default" value=""> {selectProblem} </option> @@ -68,4 +75,4 @@ function GenericProblemDropdown({ </select> </div> ); -} \ No newline at end of file +} diff --git a/src/components/spinner/Spinner.test.tsx b/src/components/spinner/Spinner.test.tsx index 3ce3ad5..beca3cc 100644 --- a/src/components/spinner/Spinner.test.tsx +++ b/src/components/spinner/Spinner.test.tsx @@ -1,35 +1,29 @@ import React from 'react'; import { render } from '@testing-library/react'; import Spinner from './Spinner'; -import each from 'jest-each'; describe('<Spinner>', () => { it('renders spinning loading indicator: default', () => { const { getByRole } = render(<Spinner />); const spinner = getByRole('progressbar'); - expect(spinner).toBeVisible(); + expect(spinner).toBeInTheDocument(); expect(spinner).not.toHaveClass('big'); }); +}); - const variations = [ - ['small', false], - ['big', true], - ]; - each(variations).it( - `renders spinning loading indicator: %s`, - (big: boolean) => { - const { getByRole } = render(<Spinner loading={true} big={big} />); - const spinner = getByRole('progressbar'); - expect(spinner).toBeVisible(); - }, - ); +describe.each([ + ['small', false], + ['big', true], +])('<Spinner>', (_, big: boolean) => { + it(`renders spinning loading indicator: %s`, () => { + const { getByRole } = render(<Spinner loading={true} big={big} />); + const spinner = getByRole('progressbar'); + expect(spinner).toBeInTheDocument(); + }); - each(variations).it( - `Does not render spinning loading indicator: %s`, - (big: boolean) => { - const { queryByRole } = render(<Spinner loading={false} big={big} />); - const spinners = queryByRole('progressbar'); - expect(spinners).toBe(null); - }, - ); + it(`Does not render spinning loading indicator: %s`, () => { + const { queryByRole } = render(<Spinner loading={false} big={big} />); + const spinners = queryByRole('progressbar'); + expect(spinners).toBe(null); + }); }); diff --git a/src/components/spinner/Spinner.tsx b/src/components/spinner/Spinner.tsx index 12698b0..6bff6b3 100644 --- a/src/components/spinner/Spinner.tsx +++ b/src/components/spinner/Spinner.tsx @@ -5,7 +5,7 @@ export interface SpinnerProps { loading?: boolean; } -function Spinner({ big = false, loading = true }: SpinnerProps) { +function Spinner({ big = false, loading = true }: SpinnerProps): JSX.Element { if (!loading) return <div className="spinner"></div>; const classes = ['spinner', 'loading']; if (big) classes.push('big'); diff --git a/src/components/urgent-request/UrgentRequest.module.scss b/src/components/urgent-request/UrgentRequest.module.scss index 0f50588..b0ae6d2 100644 --- a/src/components/urgent-request/UrgentRequest.module.scss +++ b/src/components/urgent-request/UrgentRequest.module.scss @@ -1,16 +1,3 @@ .UrgentRequest { - margin: 1rem 0 1rem 0; border: 1px rgb(113, 0, 10) solid; - position: relative; - height: 0; - overflow: hidden; - transition: all 0.2s ease-in-out; - opacity: 0; - transform-origin: top left; - transform: rotate3d(1, 0.5, 0.1, -90deg) scale(0); - &.open { - opacity: 1; - transform: rotate(0deg); - height: auto; - } } diff --git a/src/components/urgent-request/UrgentRequest.test.tsx b/src/components/urgent-request/UrgentRequest.test.tsx index 7169b71..422a9c9 100644 --- a/src/components/urgent-request/UrgentRequest.test.tsx +++ b/src/components/urgent-request/UrgentRequest.test.tsx @@ -1,29 +1,22 @@ import * as React from 'react'; -import { fireEvent, render } from '@testing-library/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(); describe('<UrgentRequest>', () => { it('renders the option to make a contact request urgent', () => { - const { getByRole, getByText } = render( - <UrgentRequest sendUrgent={mockSendUrgent} />, + const { getByRole } = render( + <UrgentRequest + sendUrgent={mockSendUrgent} + />, ); const urgentFieldset = getByRole('group'); const urgentInput = getByRole('textbox'); const urgentSubmit = getByRole('button'); - const urgentFold = getByText('Urgent request?'); - // Folded urgent form has invisible content - expect(urgentFieldset).not.toHaveClass('open'); - - // Unfolding the urgency form - userEvent.click(urgentFold); - expect(urgentFieldset).toHaveClass(styles.open); - // See expected elements expect(document.body.contains(urgentFieldset)); @@ -36,13 +29,8 @@ describe('<UrgentRequest>', () => { expect(urgentInput).toHaveValue('NOT URGENT'); expect(urgentSubmit).toBeDisabled(); - // Close the fold again.. this should clear the input field too. - userEvent.click(urgentFold); - expect(urgentFieldset).not.toHaveClass('open'); - userEvent.click(urgentFold); - expect(urgentInput).toHaveValue(''); - // Fill in URGENT to change the urgent status + userEvent.clear(urgentInput); userEvent.type(urgentInput, 'URGENT'); // State was updated.. expect(urgentInput).toHaveValue('URGENT'); diff --git a/src/components/urgent-request/UrgentRequest.tsx b/src/components/urgent-request/UrgentRequest.tsx index cf4e7b3..5e373fc 100644 --- a/src/components/urgent-request/UrgentRequest.tsx +++ b/src/components/urgent-request/UrgentRequest.tsx @@ -7,16 +7,10 @@ export interface UrgentRequestProps { sendUrgent: CallableFunction; } -function UrgentRequest({ sendUrgent }: UrgentRequestProps) { - const [open, setOpen] = useState<boolean>(false); - const [urgent, setUrgent] = useState<boolean>(true); +function UrgentRequest({ sendUrgent }: UrgentRequestProps): JSX.Element { + const [urgent, setUrgent] = useState<boolean>(false); const [urgentText, setUrgentText] = useState<string>(''); const { t } = useTranslation(); - const changeFold = (event: ChangeEvent<HTMLInputElement>) => { - setOpen(event.target.checked); - if (!open) setUrgentText(''); - }; - const changeUrgentText = (event: ChangeEvent<HTMLInputElement>): void => { setUrgentText(event.target.value); }; @@ -25,37 +19,29 @@ function UrgentRequest({ sendUrgent }: UrgentRequestProps) { setUrgent(urgentText == t('urgency.urgent')); }, [urgentText]); - function submitUrgent(event: React.SyntheticEvent<HTMLFormElement>) { - event.preventDefault(); - sendUrgent(); - } - return ( - <form onSubmit={submitUrgent}> - <div className="field.switch"> + <fieldset className={`${styles.UrgentRequest}`}> + <legend>{t('urgency.legend')}</legend> + <Markdown>{t('urgency.intro')}</Markdown> + <div className="field"> <input - id="urgency-checkbox" - type="checkbox" - className="switch fold-button" - onChange={changeFold} - /> - <label htmlFor="urgency-checkbox">{t('urgency.foldText')}</label> + type="text" + value={urgentText} + onChange={changeUrgentText} + aria-label={t('urgency.typeUrgentHere')} + ></input> </div> - <fieldset className={`${styles.UrgentRequest} ${open ? styles.open : ''}`}> - <legend>{t('urgency.legend')}</legend> - <Markdown>{t('urgency.intro')}</Markdown> - <div className="field"> - <input - type="text" - value={urgentText} - onChange={changeUrgentText} - ></input> - </div> - <button disabled={!urgent} className="bad" type="submit"> - {t('urgency.submit')} - </button> - </fieldset> - </form> + <button + disabled={!urgent} + className="bad" + onClick={(event) => { + event?.preventDefault(); + sendUrgent(); + }} + > + {t('urgency.submit')} + </button> + </fieldset> ); } diff --git a/src/config.yml b/src/config.yml index 5a75edd..f4a2403 100644 --- a/src/config.yml +++ b/src/config.yml @@ -1,5 +1,5 @@ # This determines where to mount the "app" in the DOM. -mountingElementId: "helpful-contact-form" +mountingElementId: 'helpful-contact-form' urgentRequestFee: 420 localisations: en: @@ -9,6 +9,8 @@ localisations: subtitle: Let's troubleshoot together. What type of issue are you experiencing? selectProblemGroup: Select an issue type.. selectProblem: Select issue.. + problemDescribe: 'The nature of my problem is:' + submit: Submit request urgency: legend: Confirm urgent request foldText: Urgent request? @@ -24,26 +26,32 @@ localisations: Urgent Request" submit: Submit Urgent Request urgent: URGENT + typeUrgentHere: Type URGENT in capital letters here to unlock the urgent submit button. + contact: + title: Tell us about your problem + name: "Name:" + phone: "Phone number:" + email: "E-mail address:" + message: "Your description of the problem:" + tell-us: What do you want to tell us? + details: Contact details + identification: Client identification (can be your company name, an invoice number, or a domain name) problemGroups: administrative: description: Administrative question - problemDescribe: How would you best describe your adminstrative question? - problems: - invoicing: - description: I have a question regarding an invoice - suggestionDescription: 'Here are some of the most common examples of invoice questions:' - suggestions: - - link: https://greenhost.net/contact/#payment-details - description: I need Greenhost's payment information. - legal: - description: I have a legal question - suggestionDescription: 'Here are some of the most common examples of legal questions:' - suggestions: - - link: https://greenhost.net/legal/terms-and-conditions/ - description: Where can I find your terms and conditions? + suggestionDescription: 'Here are some of the most common examples of administrative questions:' + suggestions: + - link: https://greenhost.net/contact/#payment-details + description: I need Greenhost's payment information. + - link: https://greenhost.net/legal/terms-and-conditions/ + description: Where can I find your terms and conditions? technical: description: Technical - problemDescribe: How would you best describe your adminstrative question? + problemDescribe: How would you best describe your technical question? + suggestionDescription: "Here are some solutions to some of the most common technical issues:" + suggestions: + - link: "https://greenhost.net/helpdesk/website/hosting-management/#maintenance" + description: Generic technical problems fix problems: hosting: description: My website is not reachable or is not working properly @@ -63,11 +71,13 @@ localisations: probleem ervaar je? selectProblemGroup: Selecteer een type issue.. selectProblem: Selecteer issue.. + problemDescribe: 'De aard van mijn probleem is:' + submit: Verstuur verzoek urgency: legend: Urgent verzoek bevestigen foldText: Urgent verzoek? intro: >- - LET OP: wanneer u deze optie kiest, zal uw verzoek naar het hoogste + __LET OP__: wanneer u deze optie kiest, zal uw verzoek naar het hoogste escalatieniveau worden gebracht en onmiddelijk mensen van de storingsdienst alarmeren, 24/7. @@ -75,32 +85,32 @@ localisations: tenzij we van mening zijn dat de urgentie van het issue gerechtvaardigd is door het risico op onmiddelijke, onherstelbare schade. - - Wanneer u hiermee akkoort gaat, type: "URGENT" en klik op "Verstuur urgent + + Wanneer u hiermee akkoort gaat, type: `URGENT` en klik op "Verstuur urgent verzoek" submit: Verstuur urgent verzoek urgent: URGENT + typeUrgentHere: Type hier URGENT in hoofdletters om een urgent verzoek te kunnen sturen. + contact: + title: Vertel ons over uw probleem + name: "Naam:" + phone: "Telefoonnumer:" + email: "E-mailadres:" + message: "Uw omschrijving van het probleem:" + tell-us: Wat wilt u ons vertellen? + details: Contactgegevens + identification: Klant identificatie (dit mag uw bedrijfsnaam, een factuurnummer, of een domein zijn) problemGroups: administrative: description: Administratieve vraag - problemDescribe: Hoe zou je jouw administratieve vraag het beste omschrijven? - problems: - invoicing: - description: Ik heb een vraag over een factuur - suggestionDescription: - 'Hier zijn een paar van de meest voorkomende voorbeelden - van vragen over facturen:' - suggestions: - - link: https://greenhost.nl/contact/#betalingsgegevens - description: Ik heb betalingsgegevens van Greenhost nodig - legal: - description: Ik heb een juridische vraag - suggestionDescription: - 'Hier zijn een paar van de meest voorkomende voorbeelden - juridische vragen:' - suggestions: - - link: https://greenhost.nl/juridisch/algemene-voorwaarden/ - description: Waar vind ik de algemene voorwaarden? + suggestionDescription: + 'Hier zijn een paar van de meest voorkomende voorbeelden + van administratieve vragen:' + suggestions: + - link: https://greenhost.nl/contact/#betalingsgegevens + description: Ik heb betalingsgegevens van Greenhost nodig + - link: https://greenhost.nl/juridisch/algemene-voorwaarden/ + description: Waar vind ik de algemene voorwaarden? technical: description: Technisch problemDescribe: Hoe zou je jouw technische vraag of probleem het beste omschrijven? diff --git a/src/styles/transitions/panel.module.scss b/src/styles/transitions/panel.module.scss new file mode 100644 index 0000000..ad59aca --- /dev/null +++ b/src/styles/transitions/panel.module.scss @@ -0,0 +1,27 @@ +.enter { + height: 20%; + opacity: 0; + transform: perspective(200px) rotateX(-90deg); + transform-origin: top center; +} +.exit { + opacity: 1; + height: auto; + transform: rotateX(0deg); + transform-origin: top center; +} + +.enterActive { + height: auto; + opacity: 1; + transform: rotateX(0deg); + transform-origin: top center; + transition: all 200ms ease-in-out, transform 200ms cubic-bezier(.74,.36,.61,1.53);//cubic-bezier(0.980, 0.755, 0.830, 1.515); // bounce +} +.exitActive { + height: 20%; + opacity: 0; + transform: perspective(200px) rotateX(-90deg); + transition: all 200ms ease-in-out, transform 200ms ease-in-out; + transform-origin: top center; +} diff --git a/src/tests/App.test.tsx b/src/tests/App.test.tsx new file mode 100644 index 0000000..aea792a --- /dev/null +++ b/src/tests/App.test.tsx @@ -0,0 +1,127 @@ +import * as React from 'react'; +import { act, render } 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'; + +// Use a fixture instead of the real config file. +jest.mock('../config.yml', () => require('../__mocks__/config.json')); + +describe('<App> with mocked config', () => { + it('renders with a mocked config', () => { + const { getByRole, getByText } = render(<App />); + expect(getByText('Report a problem (mocked)')).toBeInTheDocument(); + expect(getByText(/Let's troubleshoot together/i)).toBeInTheDocument(); + expect( + getByRole('button', { name: /Submit request/i }), + ).toBeInTheDocument(); + }); + + it('Helps a user find suggestions to administrative problems', () => { + const { getByRole, getByText, queryByRole } = render(<App />); + + const problemGroupInput = getByRole('combobox', { + name: 'The nature of my problem is:', + }); + expect(problemGroupInput).toBeInTheDocument(); + + expect( + queryByRole('listitem', { name: /payment info/i }), + ).not.toBeInTheDocument(); + + userEvent.selectOptions(problemGroupInput, ['administrative']); + + expect( + getByText(/examples of administrative questions/i), + ).toBeInTheDocument(); + expect(getByText(/payment info/i)).toBeInTheDocument(); + expect(getByText(/terms and conditions/i)).toBeInTheDocument(); + + // There should not be any option to select problems (hierarchically under + // problemGroups) because there is no prolem list in the fixture. + expect( + queryByRole('combobox', { + name: /How would you best describe your .* problem/i, + }), + ).not.toBeInTheDocument(); + + // For administrative issues, an urgent request can't be made. + expect( + queryByRole('checkbox', { + name: /Urgent request/i, + }), + ).not.toBeInTheDocument(); + }); + + it('Helps a user find suggestions to technical problems', () => { + const { getByText, getByRole, queryByRole, queryByText } = render(<App />); + const problemGroupInput = getByRole('combobox', { + name: 'The nature of my problem is:', + }); + + // There are no suggestions yet.. + expect(queryByRole('listitem')).not.toBeInTheDocument(); + + userEvent.selectOptions(problemGroupInput, ['technical']); + + // A list of suggestions exists under the technical problem group in the + // fixture. + expect( + getByText( + /Here are some solutions to some of the most common technical issues:/, + ), + ).toBeInTheDocument(); + expect(getByText(/Generic technical problems fix/i)).toBeInTheDocument(); + + // But a dropdown with problems is visible + const problemInput = getByRole('combobox', { + name: 'How would you best describe your technical problem?', + }); + + // We tell the form we have a hosting problem + userEvent.selectOptions(problemInput, ['hosting']); + + expect( + getByText(/ solutions to some of the most common website issues/i), + ).toBeInTheDocument(); + + expect(getByText(/Restart server processes/i)).toBeInTheDocument(); + 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(); + + // This also proves markdown works.. + expect(getByText(/CAUTION/)).toHaveStyle('font-weight: bold'); + + const input = getByRole('textbox', { + name: /Type URGENT in capital letters/i, + }); + expect(input).toBeInTheDocument(); + expect(input).toHaveValue(''); + + const submit = getByRole('button', { name: /Submit urgent request/i }); + expect(submit).toBeInTheDocument(); + expect(submit).toBeDisabled(); + + userEvent.type(input, 'NOTURGENT'); + expect(submit).toBeDisabled(); + userEvent.clear(input); + userEvent.type(input, 'URGENT'); + expect(submit).toBeEnabled(); + }); +}); diff --git a/src/types.ts b/src/types.ts index 04518cd..b527fe6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,14 +7,16 @@ export interface ProblemHierarchy { export interface ProblemGroup { description: string; problemDescribe: string; - problems: { + suggestionDescription?: string; + suggestions?: Suggestion[]; + problems?: { [problem: string]: Problem; }; } export interface Problem { description: string; - suggestionDescription: string; + suggestionDescription?: string; suggestions?: Suggestion[]; } -- GitLab