Skip to content

Commit

Permalink
converted widget store to provider
Browse files Browse the repository at this point in the history
  • Loading branch information
dn54321 committed Jul 5, 2024
1 parent 74eb73c commit f8935ac
Show file tree
Hide file tree
Showing 71 changed files with 399 additions and 213 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,4 @@ yarn-error.log*

# Others
.vscode
**/[Tt]est*
*storybook.log
8 changes: 4 additions & 4 deletions app/api/location/[location]/route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("Route: api/location/[location]", async () => {
});

const response = await GET(request, { params: { location: "mockLocation" } });
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 200 given valid location and region", async () => {
Expand All @@ -26,7 +26,7 @@ describe("Route: api/location/[location]", async () => {
});

const response = await GET(request, { params: { location: "mockLocation" } });
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 404 with empty results.", async () => {
Expand All @@ -36,7 +36,7 @@ describe("Route: api/location/[location]", async () => {
status: "ZERO_RESULTS",
});
const response = await GET(request, { params: { location: "mockLocation" } });
expect(response.status).toBe(404);
expect(response?.status).toBe(404);
});

it("should return 500 when unexpected error occurs.", async () => {
Expand All @@ -46,6 +46,6 @@ describe("Route: api/location/[location]", async () => {
});

const response = await GET(request, { params: { location: "mockLocation" } });
expect(response.status).toBe(500);
expect(response?.status).toBe(500);
});
});
6 changes: 3 additions & 3 deletions app/api/location/auto-complete/route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ describe("Route: api/location/auto-complete", async () => {
});

const response = await GET(request);
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 400 with invalid query params.", async () => {
const request = createMockRequest({ path: "/api/location/auto-complete" });
const response = await GET(request);
expect(response.status).toBe(400);
expect(response?.status).toBe(400);
});

it("should return 500 when unexpected error occurs.", async () => {
Expand All @@ -34,6 +34,6 @@ describe("Route: api/location/auto-complete", async () => {
});

const response = await GET(request);
expect(response.status).toBe(500);
expect(response?.status).toBe(500);
});
});
8 changes: 4 additions & 4 deletions app/api/location/nearby-search/route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ describe("Route: api/location/nearby-search", async () => {
});

const response = await GET(request);
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 400 with invalid query params.", async () => {
const request = createMockRequest({ path: "/api/location/nearby-search" });

const response = await GET(request);
expect(response.status).toBe(400);
expect(response?.status).toBe(400);
});

it("should return 200 when state is missing from payload.", async () => {
Expand All @@ -42,7 +42,7 @@ describe("Route: api/location/nearby-search", async () => {
});

const response = await GET(request);
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 500 when unexpected error occurs.", async () => {
Expand All @@ -53,6 +53,6 @@ describe("Route: api/location/nearby-search", async () => {
});

const response = await GET(request);
expect(response.status).toBe(500);
expect(response?.status).toBe(500);
});
});
4 changes: 2 additions & 2 deletions app/api/pollution/route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("Route: api/pollution", async () => {
});

const response = await GET(request);
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 500 when unexpected error occurs.", async () => {
Expand All @@ -31,6 +31,6 @@ describe("Route: api/pollution", async () => {
});

const response = await GET(request);
expect(response.status).toBe(500);
expect(response?.status).toBe(500);
});
});
6 changes: 3 additions & 3 deletions app/api/weather/[location]/route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("Route: api/weather/[location]", async () => {
});

const response = await GET(request, { params: { location: "mockLocation" } });
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 200 status when location and region is set.", async () => {
Expand All @@ -27,7 +27,7 @@ describe("Route: api/weather/[location]", async () => {
});

const response = await GET(request, { params: { location: "mockLocation" } });
expect(response.status).toBe(200);
expect(response?.status).toBe(200);
});

it("should return 500 when unexpected error occurs.", async () => {
Expand All @@ -37,6 +37,6 @@ describe("Route: api/weather/[location]", async () => {
});

const response = await GET(request, { params: { location: "mockLocation" } });
expect(response.status).toBe(500);
expect(response?.status).toBe(500);
});
});
2 changes: 1 addition & 1 deletion app/icons/layout.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import Layout, { weatherIconShowcase } from "./layout";
import userEvent from "@testing-library/user-event";
import { withRender } from "@utils/wrappers";
import { withRender } from "@utils/render";

describe("Page: app/icons/layout.tsx", async () => {
const mocks = vi.hoisted(() => {
Expand Down
2 changes: 1 addition & 1 deletion app/icons/page.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { weatherIconShowcase } from "./layout";
import Page from "./page";
import userEvent from "@testing-library/user-event";
import { withRender } from "@utils/wrappers";
import { withRender } from "@utils/render";

describe("Page: app/icons/page", () => {
const mocks = vi.hoisted(() => {
Expand Down
2 changes: 1 addition & 1 deletion app/icons/weather/[id]/page.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import Page from "./page";
import { withRender } from "@utils/wrappers";
import { withRender } from "@utils/render";

describe("Page: app/icons/weather/[id]", () => {
it("should correctly display the correct information for 200 weather id.", () => {
Expand Down
2 changes: 1 addition & 1 deletion app/layout.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import Layout from "./layout";
import { withRender } from "@utils/wrappers";
import { withRender } from "@utils/render";

describe("Page: layout.tsx", async () => {
it("should be able to render it's children element successfully.", () => {
Expand Down
23 changes: 13 additions & 10 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { SpeedInsights } from "@vercel/speed-insights/next";
import "three-dots/dist/three-dots.css";
import "@styles/globals.css";
import { queryClient } from "@utils/query-client";
import SettingsStoreProvider from "@components/provider/settings-provider/settings-store-provider";
import { SystemThemeProvider } from "@components/provider/system-theme-provider";
import { SettingsStoreProvider } from "@components/provider/settings-provider";
import WidgetStoreProvider from "@components/provider/widget-provider/widget-store-provider.component";

// We can't add this metadata until nextjs supports emotion on the server side.
// https://nextjs.org/docs/app/building-your-application/styling/css-in-js
Expand Down Expand Up @@ -48,15 +49,17 @@ export default function RootLayout(props: { children: React.ReactNode }) {
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<SettingsStoreProvider>
<SystemThemeProvider>
<CssBaseline />
<QueryClientProvider client={queryClient}>
{props.children}
<ReactQueryDevtools buttonPosition="bottom-left" />
<Analytics />
<SpeedInsights />
</QueryClientProvider>
</SystemThemeProvider>
<WidgetStoreProvider>
<SystemThemeProvider>
<CssBaseline />
<QueryClientProvider client={queryClient}>
{props.children}
<ReactQueryDevtools buttonPosition="bottom-left" />
<Analytics />
<SpeedInsights />
</QueryClientProvider>
</SystemThemeProvider>
</WidgetStoreProvider>
</SettingsStoreProvider>
</AppRouterCacheProvider>
</body>
Expand Down
2 changes: 1 addition & 1 deletion app/not-found.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import NotFound from "./not-found";
import { withRender } from "@utils/wrappers";
import { withRender } from "@utils/render";

describe("Page: not-found", () => {
it("should display loader when page isn't loaded.", () => {
Expand Down
2 changes: 1 addition & 1 deletion app/page.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import Home from "./page";
import { testOfflineQueryClient, testQueryClient } from "@utils/query-client";
import { withRender } from "@utils/wrappers";
import { withRender } from "@utils/render";
import { server } from "@project/vitest-setup";
import { mockWeatherHandle } from "@features/weaget/__mocks__/weather.handler";
import { mockCurrentLocationHandle } from "@features/weaget/__mocks__/current-location.handler";
Expand Down
2 changes: 1 addition & 1 deletion app/weather/[location]/page.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import Page from "./page";
import { withRender } from "@utils/wrappers";
import { withRender } from "@utils/render";
import { waitFor } from "@testing-library/react";
import { mockPollutionHandle } from "@features/weaget/__mocks__/pollution.handler";
import { mockLocationLookupHandle } from "@features/weaget/__mocks__/location-lookup.handler";
Expand Down
10 changes: 5 additions & 5 deletions app/weather/[location]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { DailyWeatherCardWidget } from "@components/widgets/daily-weather-card-w

import { WeatherDisplayWidget } from "@components/widgets/weather-display-widget";

import { useAlert } from "@src/hooks//use-alert";
import { useGetLocation } from "@src/hooks//use-get-location";
import { useGetPollution } from "@src/hooks//use-get-pollution";
import { useGetWeather } from "@src/hooks//use-get-weather";
import { useAlert } from "@src/hooks/use-alert";
import { useGetLocation } from "@src/hooks/use-get-location";
import { useGetPollution } from "@src/hooks/use-get-pollution";
import { useGetWeather } from "@src/hooks/use-get-weather";
import { useEffect } from "react";
import { useGetNearbyLocation } from "@src/hooks//use-get-nearby-location";
import { useGetNearbyLocation } from "@src/hooks/use-get-nearby-location";
import { Navbar } from "@components/layout/navbar";
import { Footer } from "@components/layout/footer";
import { SettingsFab } from "@components/ui/settings-fab";
Expand Down
26 changes: 26 additions & 0 deletions docs/achitecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Architecture

The goal of the project is to build a robust and maintable system with trying to maximise the following goals while also keeping cost to free.
- performant
- accessible
- user experience

To achieve this, the APIs need to be free, highly avaliable and consistent. To improve performance, caching mechanisms can be put in place for both client and server. To improve consistency, after processing the APIS, the APIs need to be validated, then normalised.

A high level overview of the architecture can be seen here.
![Architecture](/public/architecture.png)

## Folder structure

To support the following architecture, the files have been organised like so:

- **components** - Contains react components, styling and component test cases.
- **features** - Contains schemas, types and mocks for API endpoints.
- **services** - Server side fetching of API endpoints.
- **hooks** - Client side fetching of API endpoints, store data, and custom hooks.
- **utils** - Helper functions to reduce boilerplate code.

## Styling

When possible, please use Google's styling guide when possible:
https://google.github.io/styleguide/tsguide.html
82 changes: 82 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Testing

## Test Dependencies

Currently we use the following packages to test:

- **Vitest** - Testing framework + Coverage
- **React-Testing-Library** - Testing framework for components.
- **Faker** - Mocks data for us.
- **MSW** - Mocks all external network calls.

## Challenges

There are many things we need to remember to mock when building tests.

- Zustand Stores.
- React Query APIs.

## Recipes

### 1. Testing a component

To test a component, please render the component using the `withRender` function. This function wraps around the react-testing-library `render` function, adding the required context providers so that components can be tested in isolation.

```Typescript
it("should render children.", () => {
const { getByText } = withRender(<Layout>testText</Layout>);
expect(getByText("testText")).toBeInTheDocument();
});
```

### 2. Mocking an API endpoint

By default, all API endpoints are mocked with valid data through MSW. The mocked data resembles the actual endpoints as close as possible. However, if an API endpoint needs to be mocked with custom data, the following steps can be followed below:

1. Add the following 'afterEach' block inside unit test.
```Typescript
afterEach(() => {
testQueryClient.clear();
vi.resetAllMocks();
});
```

2. Find the handle of the corresponding API you would like to mock. Handles can be found inside *src/features*.

3. Mock API endpoint based on handle.

| Function | Example |
| ---------------- | ------------- |
| `withHandleError(handle, statusCode?: number)` | `withHandleError(mockCurrentLocationHandle, 500);` |
| `withResponse(handle)` | `withResponse(mockAutoCompleteHandle, [{ main: "mockLocation", secondary: "mockState" }])` |

### 3. Testing a Zustand store

When testing the state of a zustand store within our component, the initial state can be set with the withRender function.

```Typescript
it("Compact Weather Cards must contain the min temperature (celcius).", () => {
const settings = { temperatureScale: TemperatureScale.CELSIUS };
const { getByText } = withRender(weatherCard, { settings });
const temp = Math.round(cardProps.minTemperature - 273.15);
expect(getByText(`${temp`)).toBeInTheDocument();
});
```

If the state of the zustand store cannot be identified visually after making a series of actions, a probe can be set to view the internal state of a store.

```Typescript
async () => {
const user = userEvent.setup();
const widgetProbe = vi.fn();
const { getAllByTestId } = withRender(
<DailyWeatherCardWidget weatherData={weatherData} />,
{ probes: { widget: widgetProbe } }
);
await user.hover(getAllByTestId("weather-card")[0]);
expect(widgetProbe.mock.lastCall[0]).toMatchObject({
focusedWeather: weatherData.daily?.[0]
});
});
```
Loading

0 comments on commit f8935ac

Please sign in to comment.