From 576310288a8cb7e57e082f17596355e70edcac8b Mon Sep 17 00:00:00 2001 From: Vincent He Date: Sun, 5 Dec 2021 14:56:39 +1100 Subject: [PATCH 1/5] improve tests --- .../AddCustomer/AddCustomerForm.spec.tsx | 31 +++++++++++++++-- src/components/Customer/Customer.spec.tsx | 33 +++++++++++++++++-- src/components/Customer/Customer.tsx | 11 ++----- src/redux/reducers/customerReducers.tsx | 2 +- src/views/Home.spec.tsx | 9 +++++ src/views/Home.tsx | 7 +++- 6 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 src/views/Home.spec.tsx diff --git a/src/components/AddCustomer/AddCustomerForm.spec.tsx b/src/components/AddCustomer/AddCustomerForm.spec.tsx index cc1a2e1..f401363 100644 --- a/src/components/AddCustomer/AddCustomerForm.spec.tsx +++ b/src/components/AddCustomer/AddCustomerForm.spec.tsx @@ -1,12 +1,37 @@ -import * as React from "react"; -import { render } from "../../utils/testUtils"; +import userEvent from "@testing-library/user-event"; +import { render, screen, waitFor } from "../../utils/testUtils"; import { AddCustomerForm } from "./AddCustomerForm"; describe("", () => { + const saveCustomer = jest.fn(); + afterEach(() => { + jest.clearAllMocks() + }); it("should render a ", () => { - const wrapper = render( {}} />); + const wrapper = render(); expect(wrapper.container).toMatchSnapshot(); }); //@TODO Add tests for entering data and clicking submit + it("should render 3 input fields", () => { + render(); + expect(screen.getAllByRole("textbox").length).toEqual(3); + }); + + it("should submit a form with input value when submit is clicked", async () => { + render(); + + userEvent.type(screen.getByLabelText(/First Name/), 'Tom'); + userEvent.type(screen.getByLabelText(/Last Name/), 'Smith'); + userEvent.type(screen.getByLabelText(/Phone Number/), '0400 111 222'); + userEvent.click(screen.getByRole("button")); + await waitFor(() => { + expect(saveCustomer).toHaveBeenCalledWith({ + firstName: 'Tom', + lastName: 'Smith', + phoneNumber: '0400 111 222', + }); + }); + }); + }); diff --git a/src/components/Customer/Customer.spec.tsx b/src/components/Customer/Customer.spec.tsx index b13962c..c5836c9 100644 --- a/src/components/Customer/Customer.spec.tsx +++ b/src/components/Customer/Customer.spec.tsx @@ -1,5 +1,5 @@ -import * as React from "react"; -import { render } from "../../utils/testUtils"; +import userEvent from "@testing-library/user-event"; +import { render, screen, waitFor } from "../../utils/testUtils"; import { Customer } from "./Customer"; const customer = { @@ -10,10 +10,37 @@ const customer = { }; describe("", () => { + const deleteCustomer = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + it("should render a ", () => { const wrapper = render( - {}} /> + ); expect(wrapper.container).toMatchSnapshot(); }); + + it("should render the customer details that matches mock customer's details", () => { + render(); + expect(screen.getByRole('heading')).toHaveTextContent('Test Tester'); + expect(screen.getByText(/Phone number: /)).toHaveTextContent("Phone number: 00000000"); + }); + + it("should render a delete button", () => { + render(); + expect(screen.getByRole("button", { + name:"Delete" + })).toBeVisible(); + }); + + it("should call deleteCustomer when delete is clicked", async () => { + render(); + userEvent.click(screen.getByRole("button")); + await waitFor(() => { + expect(deleteCustomer).toHaveBeenCalledWith(customer); + }); + }) }); diff --git a/src/components/Customer/Customer.tsx b/src/components/Customer/Customer.tsx index 8fd093a..d658b93 100644 --- a/src/components/Customer/Customer.tsx +++ b/src/components/Customer/Customer.tsx @@ -11,17 +11,10 @@ import { type Props = { customer: ICustomer; - removeCustomer: (customer: ICustomer) => void; + deleteCustomer: (customer: ICustomer) => void; }; -export const Customer: React.FC = ({ customer, removeCustomer }) => { - const dispatch: Dispatch = useDispatch(); - - const deleteCustomer = React.useCallback( - (customer: ICustomer) => dispatch(removeCustomer(customer)), - [dispatch, removeCustomer] - ); - +export const Customer: React.FC = ({ customer, deleteCustomer }) => { return ( diff --git a/src/redux/reducers/customerReducers.tsx b/src/redux/reducers/customerReducers.tsx index d6d837e..d9a825d 100644 --- a/src/redux/reducers/customerReducers.tsx +++ b/src/redux/reducers/customerReducers.tsx @@ -38,7 +38,7 @@ export const customerReducer = ( }; return { ...state, - customers: state.customers.concat(newCustomer), + customers: [...state.customers, newCustomer], }; case REMOVE_CUSTOMER: const updatedCustomers: ICustomer[] = state.customers.filter( diff --git a/src/views/Home.spec.tsx b/src/views/Home.spec.tsx new file mode 100644 index 0000000..e433f47 --- /dev/null +++ b/src/views/Home.spec.tsx @@ -0,0 +1,9 @@ +import { render, screen } from "../utils/testUtils"; +import Home from "./Home"; + +describe("", () => { + it("should render three customer's details as default", () => { + render(); + expect(screen.getAllByRole("heading").length).toEqual(3); + }); +}); \ No newline at end of file diff --git a/src/views/Home.tsx b/src/views/Home.tsx index 8911b39..e55ccc2 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -19,6 +19,11 @@ const Home: React.FC = () => { [dispatch] ); + const deleteCustomer = React.useCallback( + (customer: ICustomer) => dispatch(removeCustomer(customer)), + [dispatch] + ); + return ( <> @@ -26,7 +31,7 @@ const Home: React.FC = () => { ))} From cc7197577cc13ad0b0a80eae77bb08713ac67921 Mon Sep 17 00:00:00 2001 From: Vincent He Date: Sun, 5 Dec 2021 17:05:55 +1100 Subject: [PATCH 2/5] Add unique ID to customers, validation to form & testing --- .../AddCustomer/AddCustomerForm.spec.tsx | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/src/components/AddCustomer/AddCustomerForm.spec.tsx b/src/components/AddCustomer/AddCustomerForm.spec.tsx index f401363..5e0b34a 100644 --- a/src/components/AddCustomer/AddCustomerForm.spec.tsx +++ b/src/components/AddCustomer/AddCustomerForm.spec.tsx @@ -2,29 +2,29 @@ import userEvent from "@testing-library/user-event"; import { render, screen, waitFor } from "../../utils/testUtils"; import { AddCustomerForm } from "./AddCustomerForm"; -describe("", () => { +describe('', () => { const saveCustomer = jest.fn(); afterEach(() => { jest.clearAllMocks() }); - it("should render a ", () => { + it('should render a ', () => { const wrapper = render(); expect(wrapper.container).toMatchSnapshot(); }); //@TODO Add tests for entering data and clicking submit - it("should render 3 input fields", () => { + it('should render 3 input fields', () => { render(); - expect(screen.getAllByRole("textbox").length).toEqual(3); + expect(screen.getAllByRole('textbox').length).toEqual(3); }); - it("should submit a form with input value when submit is clicked", async () => { + it('should submit a form with input value when submit is clicked', async () => { render(); userEvent.type(screen.getByLabelText(/First Name/), 'Tom'); userEvent.type(screen.getByLabelText(/Last Name/), 'Smith'); userEvent.type(screen.getByLabelText(/Phone Number/), '0400 111 222'); - userEvent.click(screen.getByRole("button")); + userEvent.click(screen.getByRole('button')); await waitFor(() => { expect(saveCustomer).toHaveBeenCalledWith({ firstName: 'Tom', @@ -34,4 +34,49 @@ describe("", () => { }); }); + describe('validation', () => { + it('should display 3 error messages when submitting a blank form', async () => { + render(); + userEvent.click(screen.getByRole('button')); + await waitFor(() => { + expect(screen.getAllByRole('alert').length).toEqual(3); + }); + }); + + it('should display an error message when first name field is touched and left empty', async () => { + render(); + userEvent.click(screen.getByLabelText(/First Name/)); + userEvent.click(screen.getByLabelText(/Last Name/)); + await waitFor(() => { + expect(screen.getByRole('alert')).toHaveTextContent('First name is required'); + }); + }); + + it('should display an error message when last name field is touched and left empty', async () => { + render(); + userEvent.click(screen.getByLabelText(/Last Name/)); + userEvent.click(screen.getByLabelText(/Phone Number/)); + await waitFor(() => { + expect(screen.getByRole('alert')).toHaveTextContent('Last name is required'); + }); + }); + + it('should display an error message when phone number field is touched and left empty', async () => { + render(); + userEvent.click(screen.getByLabelText(/Phone Number/)); + userEvent.click(screen.getByLabelText(/First Name/)); + await waitFor(() => { + expect(screen.getByRole('alert')).toHaveTextContent('Phone number is required'); + }); + }); + + it('should display an error message when inputted an invalid phone number', async () => { + render(); + userEvent.type(screen.getByLabelText(/Phone Number/), '000'); + userEvent.click(screen.getByLabelText(/First Name/)); + await waitFor(() => { + expect(screen.getByRole('alert')).toHaveTextContent('Phone number is not valid'); + }); + }); + }); }); From d6066c0544ed27519bfd656a90a5cc8efa467f31 Mon Sep 17 00:00:00 2001 From: Vincent He Date: Sun, 5 Dec 2021 17:08:47 +1100 Subject: [PATCH 3/5] Add unique ID to customers, validation to form & testing --- .../AddCustomer/AddCustomerForm.tsx | 67 ++++++++++++------- .../AddCustomer/StyledAddCustomerForm.ts | 4 ++ .../AddCustomerForm.spec.tsx.snap | 2 +- src/components/Customer/Customer.spec.tsx | 3 +- src/redux/reducers/customerReducers.spec.ts | 16 ++--- src/redux/reducers/customerReducers.tsx | 9 +-- src/types/types.ts | 2 +- src/utils/validator.ts | 7 ++ 8 files changed, 70 insertions(+), 40 deletions(-) create mode 100644 src/utils/validator.ts diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index 52ebf9e..b402255 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -6,13 +6,20 @@ import { StyledInput, StyledLabel, StyledAddButton, + StyledErrorMessage } from "./StyledAddCustomerForm"; +import { + validateEmptyField, + validatePattern +} from '../../utils/validator' type Props = { saveCustomer: (customer: ICustomer | any) => void; }; export const AddCustomerForm: React.FC = ({ saveCustomer }) => { + const PHONE_NUMBER_REGEX = /^(?:\+?(61))? ?(?:\((?=.*\)))?(0?[2-57-8])\)? ?(\d\d(?:[- ](?=\d{3})|(?!\d\d[- ]?\d[- ]))\d\d[- ]?\d[- ]?\d{3})$/ + return ( = ({ saveCustomer }) => { setSubmitting(false); }} > - - First Name - + {({ errors, touched }) => ( + + First Name + validateEmptyField(value, 'First name is required')} + /> + {errors.firstName && touched.firstName && {errors.firstName}} - Last Name - + Last Name + validateEmptyField(value, 'Last name is required')} + /> + {errors.lastName && touched.lastName && {errors.lastName}} - Phone Number - + Phone Number + + validateEmptyField(value, 'Phone number is required') || + validatePattern(value, PHONE_NUMBER_REGEX, 'Phone number is not valid')} + /> + {errors.phoneNumber && touched.phoneNumber && {errors.phoneNumber}} - Add Customer - + Add Customer + + )} ); }; diff --git a/src/components/AddCustomer/StyledAddCustomerForm.ts b/src/components/AddCustomer/StyledAddCustomerForm.ts index a7a4bb9..b1ca80f 100644 --- a/src/components/AddCustomer/StyledAddCustomerForm.ts +++ b/src/components/AddCustomer/StyledAddCustomerForm.ts @@ -30,3 +30,7 @@ export const StyledAddButton = styled.button` border-radius: 4px; font-size: 16px; `; + +export const StyledErrorMessage = styled.span` + color: red; +`; diff --git a/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap b/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap index 63bdf1a..91120de 100644 --- a/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap +++ b/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap @@ -42,7 +42,7 @@ exports[` should render a 1`] = ` class="sc-dlnjwi fstXny" id="phoneNumber" name="phoneNumber" - placeholder="john@acme.com" + placeholder="0411 222 333" type="tel" value="" /> diff --git a/src/components/Customer/Customer.spec.tsx b/src/components/Customer/Customer.spec.tsx index c5836c9..6dfe86a 100644 --- a/src/components/Customer/Customer.spec.tsx +++ b/src/components/Customer/Customer.spec.tsx @@ -1,9 +1,10 @@ import userEvent from "@testing-library/user-event"; import { render, screen, waitFor } from "../../utils/testUtils"; import { Customer } from "./Customer"; +import { v4 as uuidv4 } from 'uuid'; const customer = { - id: 1, + id: uuidv4(), firstName: "Test", lastName: "Tester", phoneNumber: "00000000", diff --git a/src/redux/reducers/customerReducers.spec.ts b/src/redux/reducers/customerReducers.spec.ts index 3b03881..bf37289 100644 --- a/src/redux/reducers/customerReducers.spec.ts +++ b/src/redux/reducers/customerReducers.spec.ts @@ -5,19 +5,19 @@ const initialState = { customers: [ { firstName: "Charles", - id: 1, + id: "1", lastName: "Babbage", phoneNumber: "0412 123 123", }, { firstName: "Alan", - id: 2, + id: "2", lastName: "Turing", phoneNumber: "(03) 9599 1234", }, { firstName: "Ada", - id: 3, + id: "3", lastName: "Lovelace", phoneNumber: "+61 423 345 567", }, @@ -37,7 +37,7 @@ describe("customer reducer", () => { type: ADD_CUSTOMER, customer: { firstName: "Test", - id: 1, + id: "1", lastName: "Dummy", phoneNumber: "000 000 000", }, @@ -46,7 +46,7 @@ describe("customer reducer", () => { customers: [ { firstName: "Test", - id: 1, + id: "1", lastName: "Dummy", phoneNumber: "000 000 000", }, @@ -60,7 +60,7 @@ describe("customer reducer", () => { type: REMOVE_CUSTOMER, customer: { firstName: "Charles", - id: 1, + id: "1", lastName: "Babbage", phoneNumber: "0412 123 123", }, @@ -69,13 +69,13 @@ describe("customer reducer", () => { customers: [ { firstName: "Alan", - id: 2, + id: "2", lastName: "Turing", phoneNumber: "(03) 9599 1234", }, { firstName: "Ada", - id: 3, + id: "3", lastName: "Lovelace", phoneNumber: "+61 423 345 567", }, diff --git a/src/redux/reducers/customerReducers.tsx b/src/redux/reducers/customerReducers.tsx index d9a825d..14e81b4 100644 --- a/src/redux/reducers/customerReducers.tsx +++ b/src/redux/reducers/customerReducers.tsx @@ -1,22 +1,23 @@ import { CustomerAction, CustomerState, ICustomer } from "../../types/types"; import { ADD_CUSTOMER, REMOVE_CUSTOMER } from "../actions/customerTypes"; +import { v4 as uuidv4 } from 'uuid'; export const initialState: CustomerState = { customers: [ { - id: 1, + id: "1", firstName: "Charles", lastName: "Babbage", phoneNumber: "0412 123 123", }, { - id: 2, + id: "2", firstName: "Alan", lastName: "Turing", phoneNumber: "(03) 9599 1234", }, { - id: 3, + id: "3", firstName: "Ada", lastName: "Lovelace", phoneNumber: "+61 423 345 567", @@ -31,7 +32,7 @@ export const customerReducer = ( switch (action.type) { case ADD_CUSTOMER: const newCustomer: ICustomer = { - id: action.customer.id ?? Math.random(), // not really unique but it's just an example + id: action.customer.id ?? uuidv4(), firstName: action.customer.firstName, lastName: action.customer.lastName, phoneNumber: action.customer.phoneNumber, diff --git a/src/types/types.ts b/src/types/types.ts index 83e2415..1b781b4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -5,7 +5,7 @@ export interface Customer { } export interface ICustomer extends Customer { - id: number; + id: string; } export type CustomerState = { diff --git a/src/utils/validator.ts b/src/utils/validator.ts new file mode 100644 index 0000000..a2ca360 --- /dev/null +++ b/src/utils/validator.ts @@ -0,0 +1,7 @@ +export const validateEmptyField = (value:string, errorMessage: string) => { + return !value ? errorMessage : undefined + } + +export const validatePattern = (value: string, pattern: RegExp, errorMessage: string) => { + return !pattern.test(value) ? errorMessage : undefined + } \ No newline at end of file From 4ab331e2364687e19d20f14c959156dc5cf83f9d Mon Sep 17 00:00:00 2001 From: Vincent He Date: Sun, 5 Dec 2021 18:57:10 +1100 Subject: [PATCH 4/5] Add filter function & tests, add cursor for buttons --- .../AddCustomer/AddCustomerForm.spec.tsx | 1 - .../AddCustomer/StyledAddCustomerForm.ts | 1 + .../AddCustomerForm.spec.tsx.snap | 2 +- src/components/Customer/StyledCustomer.ts | 1 + .../__snapshots__/Customer.spec.tsx.snap | 2 +- src/views/Home.spec.tsx | 7 +++++ src/views/Home.tsx | 29 ++++++++++++++++--- src/views/StyledHome.ts | 10 +++++++ 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/views/StyledHome.ts diff --git a/src/components/AddCustomer/AddCustomerForm.spec.tsx b/src/components/AddCustomer/AddCustomerForm.spec.tsx index 5e0b34a..a872520 100644 --- a/src/components/AddCustomer/AddCustomerForm.spec.tsx +++ b/src/components/AddCustomer/AddCustomerForm.spec.tsx @@ -12,7 +12,6 @@ describe('', () => { expect(wrapper.container).toMatchSnapshot(); }); - //@TODO Add tests for entering data and clicking submit it('should render 3 input fields', () => { render(); expect(screen.getAllByRole('textbox').length).toEqual(3); diff --git a/src/components/AddCustomer/StyledAddCustomerForm.ts b/src/components/AddCustomer/StyledAddCustomerForm.ts index b1ca80f..ed7a3ac 100644 --- a/src/components/AddCustomer/StyledAddCustomerForm.ts +++ b/src/components/AddCustomer/StyledAddCustomerForm.ts @@ -29,6 +29,7 @@ export const StyledAddButton = styled.button` border: none; border-radius: 4px; font-size: 16px; + cursor: pointer; `; export const StyledErrorMessage = styled.span` diff --git a/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap b/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap index 91120de..94a5119 100644 --- a/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap +++ b/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap @@ -47,7 +47,7 @@ exports[` should render a 1`] = ` value="" /> diff --git a/src/views/Home.spec.tsx b/src/views/Home.spec.tsx index e433f47..3e26bd3 100644 --- a/src/views/Home.spec.tsx +++ b/src/views/Home.spec.tsx @@ -1,3 +1,4 @@ +import userEvent from "@testing-library/user-event"; import { render, screen } from "../utils/testUtils"; import Home from "./Home"; @@ -6,4 +7,10 @@ describe("", () => { render(); expect(screen.getAllByRole("heading").length).toEqual(3); }); + + it('should show customer Alan Turning only if Alan is inputted in search box', () => { + render(); + userEvent.type(screen.getByRole('searchbox'), 'alan'); + expect(screen.getByRole("heading")).toHaveTextContent('Alan Turing'); + }); }); \ No newline at end of file diff --git a/src/views/Home.tsx b/src/views/Home.tsx index e55ccc2..f12e396 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -1,33 +1,54 @@ -import * as React from "react"; +import { useState, useCallback, useEffect, useMemo } from "react"; import { useSelector, shallowEqual, useDispatch } from "react-redux"; import { Customer } from "../components/Customer/Customer"; import { AddCustomerForm } from "../components/AddCustomer/AddCustomerForm"; import { Dispatch } from "redux"; import { CustomerState, ICustomer } from "../types/types"; import { addCustomer, removeCustomer } from "../redux/actions/customerActions"; +import { StyledInput } from "./StyledHome" const Home: React.FC = () => { + const [input, setInput] = useState(""); + const [customersForDisplay, setCustomersForDisplay] = useState([]); const customers: readonly ICustomer[] = useSelector( (state: CustomerState) => state.customers, shallowEqual ); + const memoedCustomers = useMemo(()=> customers, [customers]); const dispatch: Dispatch = useDispatch(); - const saveCustomer = React.useCallback( + const saveCustomer = useCallback( (customer: ICustomer) => dispatch(addCustomer(customer)), [dispatch] ); - const deleteCustomer = React.useCallback( + const deleteCustomer = useCallback( (customer: ICustomer) => dispatch(removeCustomer(customer)), [dispatch] ); + const filterCustomers = (customers: readonly ICustomer[], input: string) => customers.filter((customer) => + customer.firstName.toLowerCase().includes(input.toLowerCase()) || + customer.lastName.toLowerCase().includes(input.toLowerCase()) || + customer.phoneNumber.toLowerCase().includes(input.toLowerCase())); + + useEffect(() => { + setCustomersForDisplay(filterCustomers(memoedCustomers, input)) + }, [memoedCustomers, input]); + return ( <> - {customers.map((customer: ICustomer) => ( + { + setInput(e.target.value) + }} + /> + {customersForDisplay.map((customer: ICustomer) => ( Date: Sun, 5 Dec 2021 19:15:52 +1100 Subject: [PATCH 5/5] fix style issue --- .../AddCustomer/AddCustomerForm.spec.tsx | 60 +++++++++++-------- .../AddCustomer/AddCustomerForm.tsx | 47 ++++++++++----- src/components/Customer/Customer.spec.tsx | 20 ++++--- src/components/Customer/Customer.tsx | 2 - src/redux/reducers/customerReducers.tsx | 2 +- src/views/Home.spec.tsx | 22 +++---- src/views/Home.tsx | 39 ++++++------ src/views/StyledHome.ts | 14 ++--- 8 files changed, 120 insertions(+), 86 deletions(-) diff --git a/src/components/AddCustomer/AddCustomerForm.spec.tsx b/src/components/AddCustomer/AddCustomerForm.spec.tsx index a872520..1ff5414 100644 --- a/src/components/AddCustomer/AddCustomerForm.spec.tsx +++ b/src/components/AddCustomer/AddCustomerForm.spec.tsx @@ -2,79 +2,87 @@ import userEvent from "@testing-library/user-event"; import { render, screen, waitFor } from "../../utils/testUtils"; import { AddCustomerForm } from "./AddCustomerForm"; -describe('', () => { +describe("", () => { const saveCustomer = jest.fn(); afterEach(() => { - jest.clearAllMocks() + jest.clearAllMocks(); }); - it('should render a ', () => { + it("should render a ", () => { const wrapper = render(); expect(wrapper.container).toMatchSnapshot(); }); - it('should render 3 input fields', () => { + it("should render 3 input fields", () => { render(); - expect(screen.getAllByRole('textbox').length).toEqual(3); + expect(screen.getAllByRole("textbox").length).toEqual(3); }); - it('should submit a form with input value when submit is clicked', async () => { + it("should submit a form with input value when submit is clicked", async () => { render(); - userEvent.type(screen.getByLabelText(/First Name/), 'Tom'); - userEvent.type(screen.getByLabelText(/Last Name/), 'Smith'); - userEvent.type(screen.getByLabelText(/Phone Number/), '0400 111 222'); - userEvent.click(screen.getByRole('button')); + userEvent.type(screen.getByLabelText(/First Name/), "Tom"); + userEvent.type(screen.getByLabelText(/Last Name/), "Smith"); + userEvent.type(screen.getByLabelText(/Phone Number/), "0400 111 222"); + userEvent.click(screen.getByRole("button")); await waitFor(() => { expect(saveCustomer).toHaveBeenCalledWith({ - firstName: 'Tom', - lastName: 'Smith', - phoneNumber: '0400 111 222', + firstName: "Tom", + lastName: "Smith", + phoneNumber: "0400 111 222", }); }); }); - describe('validation', () => { - it('should display 3 error messages when submitting a blank form', async () => { + describe("validation", () => { + it("should display 3 error messages when submitting a blank form", async () => { render(); - userEvent.click(screen.getByRole('button')); + userEvent.click(screen.getByRole("button")); await waitFor(() => { - expect(screen.getAllByRole('alert').length).toEqual(3); + expect(screen.getAllByRole("alert").length).toEqual(3); }); }); - it('should display an error message when first name field is touched and left empty', async () => { + it("should display an error message when first name field is touched and left empty", async () => { render(); userEvent.click(screen.getByLabelText(/First Name/)); userEvent.click(screen.getByLabelText(/Last Name/)); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('First name is required'); + expect(screen.getByRole("alert")).toHaveTextContent( + "First name is required" + ); }); }); - it('should display an error message when last name field is touched and left empty', async () => { + it("should display an error message when last name field is touched and left empty", async () => { render(); userEvent.click(screen.getByLabelText(/Last Name/)); userEvent.click(screen.getByLabelText(/Phone Number/)); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('Last name is required'); + expect(screen.getByRole("alert")).toHaveTextContent( + "Last name is required" + ); }); }); - it('should display an error message when phone number field is touched and left empty', async () => { + it("should display an error message when phone number field is touched and left empty", async () => { render(); userEvent.click(screen.getByLabelText(/Phone Number/)); userEvent.click(screen.getByLabelText(/First Name/)); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('Phone number is required'); + expect(screen.getByRole("alert")).toHaveTextContent( + "Phone number is required" + ); }); }); - it('should display an error message when inputted an invalid phone number', async () => { + it("should display an error message when inputted an invalid phone number", async () => { render(); - userEvent.type(screen.getByLabelText(/Phone Number/), '000'); + userEvent.type(screen.getByLabelText(/Phone Number/), "000"); userEvent.click(screen.getByLabelText(/First Name/)); await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('Phone number is not valid'); + expect(screen.getByRole("alert")).toHaveTextContent( + "Phone number is not valid" + ); }); }); }); diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx index b402255..5e1b310 100644 --- a/src/components/AddCustomer/AddCustomerForm.tsx +++ b/src/components/AddCustomer/AddCustomerForm.tsx @@ -6,19 +6,17 @@ import { StyledInput, StyledLabel, StyledAddButton, - StyledErrorMessage + StyledErrorMessage, } from "./StyledAddCustomerForm"; -import { - validateEmptyField, - validatePattern -} from '../../utils/validator' +import { validateEmptyField, validatePattern } from "../../utils/validator"; type Props = { saveCustomer: (customer: ICustomer | any) => void; }; export const AddCustomerForm: React.FC = ({ saveCustomer }) => { - const PHONE_NUMBER_REGEX = /^(?:\+?(61))? ?(?:\((?=.*\)))?(0?[2-57-8])\)? ?(\d\d(?:[- ](?=\d{3})|(?!\d\d[- ]?\d[- ]))\d\d[- ]?\d[- ]?\d{3})$/ + const PHONE_NUMBER_REGEX = + /^(?:\+?(61))? ?(?:\((?=.*\)))?(0?[2-57-8])\)? ?(\d\d(?:[- ](?=\d{3})|(?!\d\d[- ]?\d[- ]))\d\d[- ]?\d[- ]?\d{3})$/; return ( = ({ saveCustomer }) => { id="firstName" name="firstName" placeholder="John" - validate={(value: string) => validateEmptyField(value, 'First name is required')} + validate={(value: string) => + validateEmptyField(value, "First name is required") + } /> - {errors.firstName && touched.firstName && {errors.firstName}} + {errors.firstName && touched.firstName && ( + + {errors.firstName} + + )} Last Name = ({ saveCustomer }) => { id="lastName" name="lastName" placeholder="Doe" - validate={(value: string) => validateEmptyField(value, 'Last name is required')} + validate={(value: string) => + validateEmptyField(value, "Last name is required") + } /> - {errors.lastName && touched.lastName && {errors.lastName}} + {errors.lastName && touched.lastName && ( + + {errors.lastName} + + )} Phone Number = ({ saveCustomer }) => { name="phoneNumber" placeholder="0411 222 333" type="tel" - validate={(value: string) => - validateEmptyField(value, 'Phone number is required') || - validatePattern(value, PHONE_NUMBER_REGEX, 'Phone number is not valid')} + validate={(value: string) => + validateEmptyField(value, "Phone number is required") || + validatePattern( + value, + PHONE_NUMBER_REGEX, + "Phone number is not valid" + ) + } /> - {errors.phoneNumber && touched.phoneNumber && {errors.phoneNumber}} + {errors.phoneNumber && touched.phoneNumber && ( + + {errors.phoneNumber} + + )} Add Customer diff --git a/src/components/Customer/Customer.spec.tsx b/src/components/Customer/Customer.spec.tsx index 6dfe86a..9596029 100644 --- a/src/components/Customer/Customer.spec.tsx +++ b/src/components/Customer/Customer.spec.tsx @@ -1,7 +1,7 @@ import userEvent from "@testing-library/user-event"; import { render, screen, waitFor } from "../../utils/testUtils"; import { Customer } from "./Customer"; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; const customer = { id: uuidv4(), @@ -12,7 +12,7 @@ const customer = { describe("", () => { const deleteCustomer = jest.fn(); - + afterEach(() => { jest.clearAllMocks(); }); @@ -26,15 +26,19 @@ describe("", () => { it("should render the customer details that matches mock customer's details", () => { render(); - expect(screen.getByRole('heading')).toHaveTextContent('Test Tester'); - expect(screen.getByText(/Phone number: /)).toHaveTextContent("Phone number: 00000000"); + expect(screen.getByRole("heading")).toHaveTextContent("Test Tester"); + expect(screen.getByText(/Phone number: /)).toHaveTextContent( + "Phone number: 00000000" + ); }); it("should render a delete button", () => { render(); - expect(screen.getByRole("button", { - name:"Delete" - })).toBeVisible(); + expect( + screen.getByRole("button", { + name: "Delete", + }) + ).toBeVisible(); }); it("should call deleteCustomer when delete is clicked", async () => { @@ -43,5 +47,5 @@ describe("", () => { await waitFor(() => { expect(deleteCustomer).toHaveBeenCalledWith(customer); }); - }) + }); }); diff --git a/src/components/Customer/Customer.tsx b/src/components/Customer/Customer.tsx index d658b93..27d5960 100644 --- a/src/components/Customer/Customer.tsx +++ b/src/components/Customer/Customer.tsx @@ -1,6 +1,4 @@ import * as React from "react"; -import { Dispatch } from "redux"; -import { useDispatch } from "react-redux"; import { ICustomer } from "../../types/types"; import { StyledCustomer, diff --git a/src/redux/reducers/customerReducers.tsx b/src/redux/reducers/customerReducers.tsx index 14e81b4..b66c7c9 100644 --- a/src/redux/reducers/customerReducers.tsx +++ b/src/redux/reducers/customerReducers.tsx @@ -1,6 +1,6 @@ import { CustomerAction, CustomerState, ICustomer } from "../../types/types"; import { ADD_CUSTOMER, REMOVE_CUSTOMER } from "../actions/customerTypes"; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; export const initialState: CustomerState = { customers: [ diff --git a/src/views/Home.spec.tsx b/src/views/Home.spec.tsx index 3e26bd3..5a69d1f 100644 --- a/src/views/Home.spec.tsx +++ b/src/views/Home.spec.tsx @@ -1,16 +1,16 @@ import userEvent from "@testing-library/user-event"; import { render, screen } from "../utils/testUtils"; -import Home from "./Home"; +import Home from "./Home"; describe("", () => { - it("should render three customer's details as default", () => { - render(); - expect(screen.getAllByRole("heading").length).toEqual(3); - }); + it("should render three customer's details as default", () => { + render(); + expect(screen.getAllByRole("heading").length).toEqual(3); + }); - it('should show customer Alan Turning only if Alan is inputted in search box', () => { - render(); - userEvent.type(screen.getByRole('searchbox'), 'alan'); - expect(screen.getByRole("heading")).toHaveTextContent('Alan Turing'); - }); -}); \ No newline at end of file + it("should show customer Alan Turning only if Alan is inputted in search box", () => { + render(); + userEvent.type(screen.getByRole("searchbox"), "alan"); + expect(screen.getByRole("heading")).toHaveTextContent("Alan Turing"); + }); +}); diff --git a/src/views/Home.tsx b/src/views/Home.tsx index f12e396..9add968 100644 --- a/src/views/Home.tsx +++ b/src/views/Home.tsx @@ -5,16 +5,18 @@ import { AddCustomerForm } from "../components/AddCustomer/AddCustomerForm"; import { Dispatch } from "redux"; import { CustomerState, ICustomer } from "../types/types"; import { addCustomer, removeCustomer } from "../redux/actions/customerActions"; -import { StyledInput } from "./StyledHome" +import { StyledInput } from "./StyledHome"; const Home: React.FC = () => { const [input, setInput] = useState(""); - const [customersForDisplay, setCustomersForDisplay] = useState([]); + const [customersForDisplay, setCustomersForDisplay] = useState( + [] + ); const customers: readonly ICustomer[] = useSelector( (state: CustomerState) => state.customers, shallowEqual ); - const memoedCustomers = useMemo(()=> customers, [customers]); + const memoedCustomers = useMemo(() => customers, [customers]); const dispatch: Dispatch = useDispatch(); @@ -28,26 +30,29 @@ const Home: React.FC = () => { [dispatch] ); - const filterCustomers = (customers: readonly ICustomer[], input: string) => customers.filter((customer) => - customer.firstName.toLowerCase().includes(input.toLowerCase()) || - customer.lastName.toLowerCase().includes(input.toLowerCase()) || - customer.phoneNumber.toLowerCase().includes(input.toLowerCase())); - + const filterCustomers = (customers: readonly ICustomer[], input: string) => + customers.filter( + (customer) => + customer.firstName.toLowerCase().includes(input.toLowerCase()) || + customer.lastName.toLowerCase().includes(input.toLowerCase()) || + customer.phoneNumber.toLowerCase().includes(input.toLowerCase()) + ); + useEffect(() => { - setCustomersForDisplay(filterCustomers(memoedCustomers, input)) + setCustomersForDisplay(filterCustomers(memoedCustomers, input)); }, [memoedCustomers, input]); return ( <> - { - setInput(e.target.value) - }} - /> + { + setInput(e.target.value); + }} + /> {customersForDisplay.map((customer: ICustomer) => (