Skip to content

Commit

Permalink
Merge pull request #24 from chiamakaikeanyi/cf-020
Browse files Browse the repository at this point in the history
Add tests
  • Loading branch information
chiamakaikeanyi authored Jun 22, 2023
2 parents 71964cf + 33511bc commit 55ade9a
Show file tree
Hide file tree
Showing 15 changed files with 259 additions and 25 deletions.
10 changes: 10 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { defineConfig } from "cypress";

export default defineConfig({
defaultCommandTimeout: 5000,
pageLoadTimeout: 10000,

component: {
devServer: {
framework: "create-react-app",
bundler: "webpack",
},
},

e2e: {
baseUrl: "http://localhost:3000",
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});
86 changes: 86 additions & 0 deletions cypress/e2e/app.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/// <reference types="cypress" />

const selectors = {
theme_switch: '[data-testid="theme_switch"]',
app_container: '[data-testid="app_container"]',
home_container: '[data-testid="home_container"]',
country_search: '[data-testid="country_search"]',
regions_filter: '[data-testid="regions"]',
countries_list: '[data-testid="countries_list"]',
details_container: '[data-testid="details_container"]',
back_button: '[data-testid="back_button"]',
border_countries: '[data-testid="border_countries"]',
border_countries_list: '[data-testid="border_countries_list"]',
empty_state_container: '[data-testid="empty_state_container"]',
};

describe("App", () => {
beforeEach(() => {
cy.visit("/");
});

it("renders without crashing", () => {
cy.get(selectors.app_container).should("exist");
});

it("toggles the theme", () => {
cy.get(selectors.theme_switch).click();
cy.get('[aria-label="Switch to dark mode"]').should("exist");
});

it("displays the search input and region filter", () => {
cy.get(selectors.country_search).should("exist");
cy.get(selectors.regions_filter).should("exist");
});

it("fetches and displays countries", () => {
cy.intercept("GET", "/v3.1/*").as("getData");

cy.wait("@getData");

cy.get(selectors.countries_list).should("have.length.above", 0);
});

it("searches for a country", () => {
cy.get(selectors.country_search).type("Germany");
cy.get(selectors.home_container).contains("Germany").should("be.visible");

cy.contains("Countries in the world - 1 country").should("be.visible");
});

it("filters countries by region", () => {
cy.get(selectors.regions_filter).select("Europe");
cy.get(selectors.home_container).contains("Europe").should("be.visible");
});

it("navigates to country details when a card is clicked", () => {
cy.get('[data-testid="deu"]').click();
cy.url().should("include", "/deu");
cy.get(selectors.details_container).should("exist");
cy.get(selectors.border_countries).should("exist");
});

it("navigates to the first border country when clicked", () => {
cy.get('[data-testid="deu"]').click();
cy.url().should("include", "/deu");
cy.get(selectors.details_container).should("exist");
cy.get(selectors.border_countries).should("exist");

cy.get(selectors.border_countries_list).find("li").first().click();
cy.url().should("include", "/nld");

// navigates back when the Back button is clicked
cy.get(selectors.back_button).click();
cy.url().should("contain", "/deu");
});
});

describe("Error", () => {
it("renders error page without crashing", () => {
cy.visit("/deut");
cy.get(selectors.empty_state_container).should("exist");

cy.wait(Cypress.config("pageLoadTimeout"));
cy.contains("An error occured. Please try again.").should("be.visible");
});
});
47 changes: 47 additions & 0 deletions cypress/integration/Details.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import { mount } from "cypress/react18";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter, Routes, Route } from "react-router-dom";
import Details from "../../src/pages/Details/Details";

const selectors = {
details_container: '[data-testid="details_container"]',
};

describe("Details", () => {
let queryClient;

beforeEach(() => {
queryClient = new QueryClient();

mount(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={["/deu"]}>
<Routes>
<Route path=":countryCode" element={<Details />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
);
});

it("renders without crashing", () => {
cy.get(selectors.details_container).should("exist");
});

it("displays country details correctly", () => {
cy.get('[data-testid="deu"]')
.scrollIntoView();
cy.contains("Germany").should("be.visible");
cy.contains("Native Name:").should("be.visible");
cy.contains("Population").should("be.visible");
cy.contains("Region").should("be.visible");
cy.contains("Sub Region").should("be.visible");
cy.contains("Capital").should("be.visible");
cy.contains("Top Level Domain").should("be.visible");
cy.contains("Currencies").should("be.visible");
cy.contains("Languages").should("be.visible");
cy.contains("Driver's Side").should("be.visible");
cy.contains("Timezone(s)").should("be.visible");
});
});
35 changes: 30 additions & 5 deletions cypress/integration/Home.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,40 @@
import React from "react";
import { mount } from "cypress/react18";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router-dom";
import Home from "../../src/pages/Home/Home";

const selectors = {
container: "[data-testid='home_container']",
home_container: "[data-testid='home_container']",
};

describe("Home", () => {
it("renders", () => {
// see: https://on.cypress.io/mounting-react
mount(<Home />);
cy.get(selectors.container).should("be.visible");
let queryClient;

beforeEach(() => {
queryClient = new QueryClient();
mount(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<Home />
</MemoryRouter>
</QueryClientProvider>
);
});

it("renders without crashing", () => {
cy.get(selectors.home_container).should("exist");
cy.contains("Countries in the world - More than 200 countries").should(
"be.visible"
);
});

it("displays the countries", () => {
cy.get('[data-testid="deu"]')
.scrollIntoView();
cy.contains("Germany").should("be.visible");
cy.contains("Population: 83,240,525").should("be.visible");
cy.contains("Region: Europe").should("be.visible");
cy.contains("Capital: Berlin").should("be.visible");
});
});
20 changes: 20 additions & 0 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')
23 changes: 13 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import Details from "./pages/Details/Details";
import EmptyState from "./components/EmptyState/EmptyState";
import "./App.scss";

const queryClient = new QueryClient(
{
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 60, // 60 minutes
},
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 60, // 60 minutes
},
}
);
},
});

function App() {
const { isOnline } = useNetworkStatus();
Expand All @@ -29,7 +27,7 @@ function App() {
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<BrowserRouter>
<div className="container">
<div className="container" data-testid="app_container">
<a href="#main" className="skip-to-main-content">
Skip to main content
</a>
Expand All @@ -39,7 +37,12 @@ function App() {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/:countryCode" element={<Details />} />
<Route path="*" element={<p>Error</p>} />
<Route
path="*"
element={
<EmptyState message="An error occured. Please try again." />
}
/>
</Routes>
) : (
<EmptyState message="No internet connection" />
Expand Down
3 changes: 3 additions & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@ interface IProps {
customClass?: string;
icon?: ReactNode;
label: string;
testId?: string;
onClick?: MouseEventHandler<HTMLElement>;
}

const Button: React.FC<IProps> = ({
customClass = "",
icon,
label,
testId,
onClick,
...rest
}) => {
return (
<button
className={composeClass(styles.button, customClass)}
onClick={onClick}
data-testid={testId}
{...rest}
>
{icon ? <span className={styles.icon}>{icon}</span> : null}
Expand Down
4 changes: 3 additions & 1 deletion src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface IProps {
population: number;
region: string;
capital: string;
cca3: string;
}

const Card: React.FC<IProps> = ({
Expand All @@ -18,9 +19,10 @@ const Card: React.FC<IProps> = ({
population,
region,
capital,
cca3,
}) => {
return (
<article className={styles.container}>
<article className={styles.container} data-testid={cca3}>
<div className={styles.card}>
<img
src={flags.svg}
Expand Down
4 changes: 2 additions & 2 deletions src/components/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ interface IProps {

export const EmptyState: React.FC<IProps> = ({ message }) => {
return (
<div className={styles.container}>
<div className={styles.container} data-testid="empty_state_container">
<Player
src="https://assets4.lottiefiles.com/packages/lf20_RiLwoG.json"
loop
autoplay
speed={1}
style={{ width: "300px", height: "300px" }}
/>
<h2 className={styles.content}>{message}</h2>
<h2 className={styles.content}>{message}</h2>
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const Header: React.FC = () => {
}
icon={theme === "dark" ? <SunIcon color="#fff" /> : <MoonIcon />}
customClass={styles.theme_switch}
testId="theme_switch"
onClick={handleThemeChange}
/>
</header>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import styles from "./Input.module.scss";

interface IProps {
name: string;
testId: string;
placeholder?: string;
type?: string;
value?: string;
Expand All @@ -30,6 +31,7 @@ const Input: React.FC<IProps> = ({
onChange,
autoComplete = "off",
maxLength = 20,
testId
}) => {
return (
<div className={composeClass(styles.container, customClass)}>
Expand All @@ -48,6 +50,7 @@ const Input: React.FC<IProps> = ({
className={styles.input}
autoComplete={autoComplete}
maxLength={maxLength}
data-testid={testId}
onChange={onChange}
/>
</div>
Expand Down
16 changes: 14 additions & 2 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,28 @@ interface IProps {
placeholder: string;
options: string[];
name: string;
testId: string;
onChange: ChangeEventHandler<HTMLSelectElement>;
}

const Select: React.FC<IProps> = ({ placeholder, options, name, onChange }) => {
const Select: React.FC<IProps> = ({
placeholder,
options,
name,
testId,
onChange,
}) => {
return (
<div className={styles.container}>
<label htmlFor={name} className="visually-hidden">
Select region
</label>
<select onChange={onChange} className={styles.select} id={name}>
<select
onChange={onChange}
className={styles.select}
id={name}
data-testid={testId}
>
<option value="">{placeholder}</option>
{options?.map((option, index) => (
<option key={index} value={option}>
Expand Down
Loading

0 comments on commit 55ade9a

Please sign in to comment.