Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kkocis - PR #30

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
]
},
"devDependencies": {
"jest-fetch-mock": "^3.0.3"
"@types/react-css-modules": "^4.6.8",
"jest-fetch-mock": "^3.0.3",
"react-router-dom": "^6.22.3"
}
}
15 changes: 11 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import React, { Component } from "react";
import { Component } from "react";
import HomePage from "./components/HomePage";

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { EstablishmentDetailPage } from './components/EstablishmentDetailPage';
class App extends Component {
render() {
return <HomePage />;
}
return(
<BrowserRouter>
<Routes>
<Route index element={<HomePage />} />
<Route path="/detail/:id" element={<EstablishmentDetailPage />} />
</Routes>
</BrowserRouter>
)}
}

export default App;
49 changes: 48 additions & 1 deletion src/api/ratingsAPI.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { enableFetchMocks } from "jest-fetch-mock";
import { getEstablishmentRatings } from "./ratingsAPI";
import {
getEstablishmentRatings,
getEstablishmentDetails,
getAuthorities,
filterEstablishmentsByAuthority
} from "./ratingsAPI";
import fetch from "jest-fetch-mock";

enableFetchMocks();
Expand All @@ -24,4 +29,46 @@ describe("Ratings API", () => {
`http://api.ratings.food.gov.uk/Establishments/basic/${pageNum}/10`
);
});

it("call the ratings api with the provided id and returns the data", async () => {
const id = "123";
const expected = { testing: "test detail" };
fetch.mockResponseOnce(JSON.stringify(expected));

const actual = await getEstablishmentDetails(id);

expect(actual).toEqual(expected);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual(
`http://api.ratings.food.gov.uk/establishments/${id}`
);
});

it("call the ratings api Authorities and returns the data", async () => {
const expected = { authorities: "test authority" };
fetch.mockResponseOnce(JSON.stringify(expected));

const actual = await getAuthorities();

expect(actual).toEqual(expected);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual(
`http://api.ratings.food.gov.uk/Authorities`
);
});

it("call the ratings api with provided pageNumber, authorityId and filters the establisments", async () => {
const authorityId = 4321;
const pageNum = 1;
const expected = { testing: "test" };
fetch.mockResponseOnce(JSON.stringify(expected));

const actual = await filterEstablishmentsByAuthority(authorityId, pageNum);

expect(actual).toEqual(expected);
expect(fetch.mock.calls.length).toEqual(1);
expect(fetch.mock.calls[0][0]).toEqual(
`http://api.ratings.food.gov.uk/Establishments?pageNumber=${pageNum}&pageSize=10&localAuthorityId=${authorityId}`
);
});
});
63 changes: 62 additions & 1 deletion src/api/ratingsAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,72 @@ export type EstablishmentsType = {
];
};

export function getEstablishmentRatings(
export type AuthoritiesType = {
authorities: {}[];
meta: {
dataSource: string;
extractDate: string;
itemCount: number;
returncode: string;
totalCount: number;
totalPages: number;
pageSize: number;
pageNumber: number;
};
links: [
{
rel: string;
href: string;
}
];
};

export type EstablishmentDetailType = {
AddressLine1: string,
AddressLine2: string,
AddressLine3: string,
AddressLine4: string,
BusinessName: string,
businessTypeID: number,
FHRSID: number,
localAuthorityBusinessID: string,
PostCode: string,
RatingDate: string,
RatingValue: string,
scores: {}
}

export async function getEstablishmentRatings(
pageNum: number
): Promise<EstablishmentsType> {
return fetch(
`http://api.ratings.food.gov.uk/Establishments/basic/${pageNum}/10`,
{ headers: { "x-api-version": "2" } }
).then((res) => res.json());
}

export async function getEstablishmentDetails(
id: string | undefined
): Promise<EstablishmentDetailType> {
return fetch(
`http://api.ratings.food.gov.uk/establishments/${id}`,
{ headers: { "x-api-version": "2" } }
).then((res) => res.json());
}

export async function getAuthorities(): Promise<AuthoritiesType> {
return fetch(
`http://api.ratings.food.gov.uk/Authorities`,
{ headers: { "x-api-version": "2" } }
).then((res) => res.json());
}

export async function filterEstablishmentsByAuthority(
authorityId: number,
pageNum: number
): Promise<EstablishmentsType> {
return fetch(
`http://api.ratings.food.gov.uk/Establishments?pageNumber=${pageNum}&pageSize=10&localAuthorityId=${authorityId}`,
{ headers: { "x-api-version": "2" } }
).then((res) => res.json());
}
39 changes: 39 additions & 0 deletions src/components/DropdownFilter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Dropdown from './DropdownFilter';

describe("DropdownFilter ", ()=> {
interface Option {
label: string;
value: string;
}

const options: Option[] = [
{ label: 'Option 1', value: 'value1' },
{ label: 'Option 2', value: 'value2' },
{ label: 'Option 3', value: 'value3' },
];

it('renders dropdown with options and selects default value', () => {
const mockOnChange = jest.fn();
render(<Dropdown options={options} defaultValue="value2" onChange={mockOnChange} />);

const select = screen.getByRole('listbox');
expect(select).toBeInTheDocument();
expect(select).toHaveValue('value2');
});

it('handles user selection and calls onChange callback', () => {
const mockOnChange = jest.fn();
render(<Dropdown options={options} onChange={mockOnChange} />);

const select = screen.getByRole('listbox');
userEvent.selectOptions(select, 'value3');

expect(select).toHaveValue('value3');
expect(mockOnChange).toHaveBeenCalledTimes(1);
expect(mockOnChange).toHaveBeenCalledWith('value3');
});

})

33 changes: 33 additions & 0 deletions src/components/DropdownFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { useState } from 'react';
import styles from '../styles/DropdownFilter.module.css'
interface Option {
label: string;
value: string;
}

interface DropdownProps {
options: Option[];
defaultValue?: string;
onChange: (value: string) => void;
}

const Dropdown: React.FC<DropdownProps> = ({ options, defaultValue, onChange }) => {
const [selectedValue, setSelectedValue] = useState(defaultValue || options[0].value);

const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedValue(event.target.value);
onChange(event.target.value);
};

return (
<select role="listbox" value={selectedValue} onChange={handleChange} className={styles.minimal}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};

export default Dropdown;
62 changes: 62 additions & 0 deletions src/components/EstablishmentDetailPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { render, screen, waitFor, act } from "@testing-library/react";
import { EstablishmentDetailPage } from "./EstablishmentDetailPage";
import { MemoryRouter } from "react-router-dom";
import { enableFetchMocks } from "jest-fetch-mock";
import fetch from "jest-fetch-mock";

describe("EstablishmentDetailPage", () => {
beforeEach(() => {
enableFetchMocks();
fetch.resetMocks();
});

it("renders EstablishmentDetailPage and checks loading indicator", () => {
act(() => {
render(
<MemoryRouter initialEntries={["/detail/123"]}>
<EstablishmentDetailPage />
</MemoryRouter>
);
})

const spinner = screen.getByRole("status");
expect(spinner).toBeInTheDocument();
});

it("renders EstablishmentDetailPage with details", async () => {
const expected = {
AddressLine1: "Camden Street",
AddressLine2: "192-198 Camden",
AddressLine3: "",
AddressLine4: "London",
BusinessName: "Test Establisment",
PostCode: "123456",
RatingDate: "2023-10-12T00:00:00",
RatingValue: "2",
scores: {
Hygiene: 15,
Structural: 10,
ConfidenceInManagement: 10,
},
};
fetch.mockResponseOnce(JSON.stringify(expected));

await act(async () => {
render(
<MemoryRouter initialEntries={["/detail/123"]}>
<EstablishmentDetailPage />
</MemoryRouter>
);
await waitFor(() => expect(screen.getByTestId("detailPage")).toBeInTheDocument());
})

expect(screen.getByTestId("detailPage")).toMatchSnapshot();
expect(screen.getByTestId("fullAddress").textContent).toEqual(
"Address: Camden Street 192-198 Camden, 123456 London"
);
expect(screen.getByTestId("dateOfInspection").textContent).toEqual(
"Date of inspection: 10/12/2023"
);
expect(screen.getByTestId("rating").textContent).toEqual("Rating: 2");
});
});
79 changes: 79 additions & 0 deletions src/components/EstablishmentDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import styles from "../styles/EstablishmentDetailPage.module.css";
import {
getEstablishmentDetails,
EstablishmentDetailType,
} from "../api/ratingsAPI";
import LoadingSpinner from "./LoadingSpinner";
import GenericButton from "./GenericButton";

export const EstablishmentDetailPage = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<{
message: string;
[key: string]: string;
}>();
const [details, setDetails] = useState<EstablishmentDetailType>();

useEffect(() => {
getEstablishmentDetails(id)
.then(
(result) => {
setDetails(result);
},
(error) => {
setError(error);
}
)
.finally(() => {
setLoading(false);
});
}, []);

const navigateBack = () => {
navigate("/");
};
const formatedDate = (date: string | undefined) => {
return date ? new Date(date).toLocaleDateString() : "N/A";
};

const displayRating = (values: {} | undefined) => {
const entries = values ? Object.entries(values) : [];
return (
<ul>
{entries.map((entry) => (
<li key={entry[0]}>
<p> {entry[0]}: {entry[1]}</p>
</li>
))}
</ul>
);
};

return (
<div className={styles.detailTable}>
{loading ? (
<LoadingSpinner message="Loading..." />
) : (
<div data-testid="detailPage" className={styles.details}>
<h1> {details?.BusinessName} </h1>
<p data-testid="fullAddress">
<b>Address:</b>{` ${details?.AddressLine1} ${details?.AddressLine2}${details?.AddressLine3.toString() === "" ? "," : " "+details?.AddressLine3+","} ${details?.PostCode} ${details?.AddressLine4}`}
</p>
<p data-testid="dateOfInspection">
<b>Date of inspection:</b> {formatedDate(details?.RatingDate)}
</p>
<div>
<p data-testid="rating"><b>Rating:</b>{" "}{details?.RatingValue}</p>
</div>
{displayRating(details?.scores)}

<GenericButton text="Go Back" onClick={navigateBack} />
</div>
)}
</div>
);
};
Loading