diff --git a/src/components/AddCustomer/AddCustomerForm.spec.tsx b/src/components/AddCustomer/AddCustomerForm.spec.tsx
index cc1a2e1..1ff5414 100644
--- a/src/components/AddCustomer/AddCustomerForm.spec.tsx
+++ b/src/components/AddCustomer/AddCustomerForm.spec.tsx
@@ -1,12 +1,89 @@
-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",
+ });
+ });
+ });
+
+ 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"
+ );
+ });
+ });
+ });
});
diff --git a/src/components/AddCustomer/AddCustomerForm.tsx b/src/components/AddCustomer/AddCustomerForm.tsx
index 52ebf9e..5e1b310 100644
--- a/src/components/AddCustomer/AddCustomerForm.tsx
+++ b/src/components/AddCustomer/AddCustomerForm.tsx
@@ -6,13 +6,18 @@ 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..ed7a3ac 100644
--- a/src/components/AddCustomer/StyledAddCustomerForm.ts
+++ b/src/components/AddCustomer/StyledAddCustomerForm.ts
@@ -29,4 +29,9 @@ export const StyledAddButton = styled.button`
border: none;
border-radius: 4px;
font-size: 16px;
+ cursor: pointer;
+`;
+
+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..94a5119 100644
--- a/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap
+++ b/src/components/AddCustomer/__snapshots__/AddCustomerForm.spec.tsx.snap
@@ -42,12 +42,12 @@ 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/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 d6d837e..b66c7c9 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,14 +32,14 @@ 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,
};
return {
...state,
- customers: state.customers.concat(newCustomer),
+ customers: [...state.customers, newCustomer],
};
case REMOVE_CUSTOMER:
const updatedCustomers: ICustomer[] = state.customers.filter(
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
diff --git a/src/views/Home.spec.tsx b/src/views/Home.spec.tsx
new file mode 100644
index 0000000..5a69d1f
--- /dev/null
+++ b/src/views/Home.spec.tsx
@@ -0,0 +1,16 @@
+import userEvent from "@testing-library/user-event";
+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);
+ });
+
+ 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 8911b39..9add968 100644
--- a/src/views/Home.tsx
+++ b/src/views/Home.tsx
@@ -1,32 +1,63 @@
-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 = 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) => (
))}
>
diff --git a/src/views/StyledHome.ts b/src/views/StyledHome.ts
new file mode 100644
index 0000000..4c14461
--- /dev/null
+++ b/src/views/StyledHome.ts
@@ -0,0 +1,10 @@
+import styled from "styled-components";
+
+export const StyledInput = styled.input`
+ margin: 1rem 0 1rem;
+ padding: 0.5rem;
+ font-size: 16px;
+ align-self: flex-start;
+ width: 20rem;
+ text-align: center;
+`;