# Prerequisite

Kahraman ve kötü adam bileşen grupları boyunca kullanılacak olan yeni arayüzler ve türler oluşturun.

```typescript
// src/models/Boy.ts
export interface Boy {
  id: string;
  name: string;
  description: string;
}

/* istanbul ignore file */
```

```typescript
// src/models/types.ts
import { Hero } from "./Hero";
import { Villain } from "./Villain";
import { Boy } from "./Boy";

export type HeroProperty = Hero["name"] | Hero["description"] | Hero["id"];
export type VillainProperty =
  | Villain["name"]
  | Villain["description"]
  | Villain["id"];

export type BoyProperty = Boy["name"] | Boy["description"] | Boy["id"];
export type EntityRoute = "heroes" | "villains" | "boys";
export type EntityType = "hero" | "villain" | "boy";

/** Returns the corresponding route for the entity;
 *
 * `hero` -> `/heroes`, `villain` -> `/villains`, `boy` -> `/boys` */
export const entityRoute = (entityType: EntityType) =>
  entityType === "hero"
    ? "heroes"
    : entityType === "villain"
    ? "villains"
    : "boys";

/* istanbul ignore file */
```

## `api.ts`'yi kendi klasörüne taşıyın

`./src/hooks/api.ts` adresinden `./src/api/api.ts` adresine. IDE'niz bağımlılıkları otomatik olarak güncellemelidir.

## Hook'ları güncelleyin

`useDeleteEntity`, `Boy` için güncellenir.

```typescript
// src/hooks/useDeleteEntity.ts
import { Boy } from "models/Boy";
import { Hero } from "models/Hero";
import { EntityType, entityRoute } from "models/types";
import { Villain } from "models/Villain";
import { useMutation, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import { deleteItem } from "../api/api";

/**
 * Helper for DELETE to `/heroes`, `/villains` or 'boys' routes.
 * @returns {object} {deleteEntity, isDeleting, isDeleteError, deleteError}
 */
export function useDeleteEntity(entityType: EntityType) {
  const route = entityRoute(entityType);
  const navigate = useNavigate();
  const queryClient = useQueryClient();

  const mutation = useMutation(
    (item: Hero | Villain | Boy) => deleteItem(`${route}/${item.id}`),
    {
      // on success receives the original item as a second argument
      // if you recall, the first argument is the created item
      onSuccess: (_, deletedEntity: Hero | Villain | Boy) => {
        // get all the entities from the cache
        const entities: Hero[] | Villain[] | Boy[] =
          queryClient.getQueryData([`${route}`]) || [];
        // set the entities cache without the delete one
        queryClient.setQueryData(
          [`${route}`],
          entities.filter((h) => h.id !== deletedEntity.id)
        );

        navigate(`/${route}`);
      },
    }
  );

  return {
    deleteEntity: mutation.mutate,
    isDeleting: mutation.isLoading,
    isDeleteError: mutation.isError,
    deleteError: mutation.error,
  };
}
```

`useGetEntities` yalnızca yorumun güncellenmesi gereklidir

* *`/heroes` veya `/villains` yollarına GET için yardımcı*
* *`/heroes`, `/villains` veya `/boys` yollarına GET için yardımcı*.

`usePostEntity` `Boy` için güncellenir.

```typescript
// src/hooks/usePostEntity.ts
import { Boy } from "models/Boy";
import { Hero } from "models/Hero";
import { EntityType, entityRoute } from "models/types";
import { Villain } from "models/Villain";
import { useMutation, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import { createItem } from "../api/api";

/**
 * Helper for simple POST to `/heroes`, `/villains`, `/boys` routes
 * @returns {object} {mutate, status, error}
 */
export function usePostEntity(entityType: EntityType) {
  const route = entityRoute(entityType);
  const queryClient = useQueryClient();
  const navigate = useNavigate();
  return useMutation((item: Hero | Villain | Boy) => createItem(route, item), {
    onSuccess: (newData: Hero | Villain | Boy) => {
      //  use queryClient's setQueryData to set the cache
      // takes a key as the first arg, the 2nd arg is a cb that takes the old query cache and returns the new one
      queryClient.setQueryData(
        [route],
        (oldData: Hero[] | Villain[] | Boy[] | undefined) => [
          ...(oldData || []),
          newData,
        ]
      );

      return navigate(`/${route}`);
    },
  });
}
```

`usePutEntity` `Boys` için güncellenir.

```typescript
// src/hooks/usePutEntity.ts
import { Hero } from "models/Hero";
import { Boy } from "models/Boy";
import { Villain } from "models/Villain";
import { EntityType, entityRoute } from "models/types";
import { useMutation, useQueryClient } from "react-query";
import type { QueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import { editItem } from "../api/api";

/**
 * Helper for PUT to `/heroes` route
 * @returns {object} {updateHero, isUpdating, isUpdateError, updateError}
 */
export function usePutEntity(entityType: EntityType) {
  const route = entityRoute(entityType);
  const queryClient = useQueryClient();
  const navigate = useNavigate();
  const mutation = useMutation(
    (item: Hero | Villain | Boy) => editItem(`${route}/${item.id}`, item),
    {
      onSuccess: (updatedEntity: Hero | Villain | Boy) => {
        updateEntityCache(entityType, updatedEntity, queryClient);
        navigate(`/${route}`);
      },
    }
  );

  return {
    updateEntity: mutation.mutate,
    isUpdating: mutation.isLoading,
    isUpdateError: mutation.isError,
    updateError: mutation.error,
  };
}

/** Replace a hero in the cache with the updated version. */
function updateEntityCache(
  entityType: EntityType,
  updatedEntity: Hero | Villain | Boy,
  queryClient: QueryClient
) {
  const route = entityRoute(entityType);
  // get all the heroes from the cache
  let entityCache: Hero[] | Villain[] | Boy[] =
    queryClient.getQueryData(route) || [];

  // find the index in the cache of the hero that's been edited
  const entityIndex = entityCache.findIndex((h) => h.id === updatedEntity.id);

  if (entityIndex !== -1) {
    // if the entity is found, replace the pre-edited entity with the updated one
    // this is just replacing an array item in place,
    // while not mutating the original array
    entityCache = entityCache.map((preEditedEntity) =>
      preEditedEntity.id === updatedEntity.id ? updatedEntity : preEditedEntity
    );
    console.log("entityCache is", entityCache);
    // use queryClient's setQueryData to set the cache
    // takes a key as the first arg, the 2nd arg is the new cache
    return queryClient.setQueryData([route], entityCache);
  }
}
```

## E2E komutlarını değiştirin

`./cypress/support` altındaki 3 dosya arasında, alakalılık lehine içe aktarmaları bölmek mümkündür

* `commands.ts`: tüm eklenti içe aktarmaları ve e2e & CT ile ortak komutlar (ör: `cy.getByCY`).
* `component.ts`: bileşen testlerine özgü komutlar (ör: `cy.mount`, `cy.wrappedMount`).
* `e2e.ts`: e2e'ye özgü komutlar (ör: `cy.crud`).

```typescript
// cypress/support/commands.ts
import "@testing-library/cypress/add-commands";
import "@bahmutov/cypress-code-coverage/support";

Cypress.Commands.add("getByCy", (selector, ...args) =>
  cy.get(`[data-cy="${selector}"]`, ...args)
);

Cypress.Commands.add("getByCyLike", (selector, ...args) =>
  cy.get(`[data-cy*=${selector}]`, ...args)
);

Cypress.Commands.add("getByClassLike", (selector, ...args) =>
  cy.get(`[class*=${selector}]`, ...args)
);
```

`component.tsx` ve `e2e.ts`, CT ve e2e dosyalarının ortak komutlara ve eklentilere erişimi olması için `commands.ts`'yi içe aktaracaktır.

```tsx
// cypress/support/component.tsx
import "./commands";
import { mount } from "cypress/react18";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { ErrorBoundary } from "react-error-boundary";
import ErrorComp from "../../src/components/ErrorComp";
import PageSpinner from "../../src/components/PageSpinner";
import { Suspense } from "react";

Cypress.Commands.add("mount", mount);

Cypress.Commands.add(
  "wrappedMount",
  (WrappedComponent: React.ReactNode, options = {}) => {
    const wrapped = (
      <QueryClientProvider client={new QueryClient()}>
        <ErrorBoundary fallback={<ErrorComp />}>
          <Suspense fallback={<PageSpinner />}>
            <BrowserRouter>{WrappedComponent}</BrowserRouter>
          </Suspense>
        </ErrorBoundary>
      </QueryClientProvider>
    );
    return cy.mount(wrapped, options);
  }
);
```

```typescript
// cypress/support/e2e.ts
import "./commands";
import { Villain } from "./../../src/models/Villain";
import { Hero } from "../../src/models/Hero";
import { Boy } from "../../src/models/Boy";
import {
  entityRoute,
  EntityRoute,
  EntityType,
  HeroProperty,
  VillainProperty,
  BoyProperty,
} from "../../src/models/types";
import data from "../fixtures/db.json";

Cypress.Commands.add(
  "crud",
  (
    method: "GET" | "POST" | "PUT" | "DELETE",
    route: string,
    {
      body,
      allowedToFail = false,
    }: { body?: Hero | Villain | Boy | object; allowedToFail?: boolean } = {}
  ) =>
    cy.request<(Hero[] & Hero) | (Villain[] & Villain) | (Boy[] & Boy)>({
      method: method,
      url: `${Cypress.env("API_URL")}/${route}`,
      body: method === "POST" || method === "PUT" ? body : undefined,
      retryOnStatusCodeFailure: !allowedToFail,
      failOnStatusCode: !allowedToFail,
    })
);

Cypress.Commands.add("resetData", () =>
  cy.crud("POST", "reset", { body: data })
);

const { _ } = Cypress;

const propExists =
  (property: HeroProperty | VillainProperty | BoyProperty) =>
  (entity: Hero | Villain | Boy) =>
    entity.name === property ||
    entity.description === property ||
    entity.id === property;

const getEntities = (entityRoute: EntityRoute) =>
  cy.crud("GET", entityRoute).its("body");

Cypress.Commands.add(
  "getEntityByProperty",
  (
    entityType: EntityType,
    property: HeroProperty | VillainProperty | BoyProperty
  ) =>
    getEntities(entityRoute(entityType)).then((entities) =>
      _.find(entities, propExists(property))
    )
);

Cypress.Commands.add(
  "findEntityIndex",
  (
    entityType: EntityType,
    property: HeroProperty | VillainProperty | BoyProperty
  ) =>
    getEntities(entityRoute(entityType)).then(
      (body: Hero[] | Villain[] | Boy[]) => ({
        entityIndex: _.findIndex(body, propExists(property)),
        entityArray: body,
      })
    )
);

Cypress.Commands.add("visitStubbedEntities", (entityRoute: EntityRoute) => {
  cy.intercept("GET", `${Cypress.env("API_URL")}/${entityRoute}`, {
    fixture: `${entityRoute}.json`,
  }).as(`stubbed${_.startCase(entityRoute)}`);
  cy.visit(`/${entityRoute}`);
  cy.wait(`@stubbed${_.startCase(entityRoute)}`);
  return cy.location("pathname").should("eq", `/${entityRoute}`);
});

Cypress.Commands.add("visitEntities", (entityRoute: EntityRoute) => {
  cy.intercept("GET", `${Cypress.env("API_URL")}/${entityRoute}`).as(
    `get${_.startCase(entityRoute)}`
  );
  cy.visit(`/${entityRoute}`);
  cy.wait(`@get${_.startCase(entityRoute)}`);
  return cy.location("pathname").should("eq", `/${entityRoute}`);
});
```

Repo kökündeki tip tanımlarını güncelleyin.

````typescript
// cypress.d.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import { MountOptions, MountReturn } from "cypress/react";
import { HeroProperty, VillainProperty, EntityType } from "models/types";
import type { Hero } from "./models/types/Hero";
import type { Villain } from "./models/types/Villain";
import type { Boy } from "./models/types/Boy";

export {};
declare global {
  namespace Cypress {
    interface Chainable {
      /** Yields elements with a data-cy attribute that matches a specified selector.
       * ```
       * cy.getByCy('search-toggle') // where the selector is [data-cy="search-toggle"]
       * ```
       */
      getByCy(qaSelector: string, args?: any): Chainable<JQuery<HTMLElement>>;

      /** Yields elements with data-cy attribute that partially matches a specified selector.
       * ```
       * cy.getByCyLike('chat-button') // where the selector is [data-cy="chat-button-start-a-new-claim"]
       * ```
       */
      getByCyLike(
        qaSelector: string,
        args?: any
      ): Chainable<JQuery<HTMLElement>>;

      /** Yields the element that partially matches the css class
       * ```
       * cy.getByClassLike('StyledIconBase') // where the class is class="StyledIconBase-ea9ulj-0 lbJwfL"
       * ```
       */
      getByClassLike(
        qaSelector: string,
        args?: any
      ): Chainable<JQuery<HTMLElement>>;

      /** Mounts a React node
       * @param component React Node to mount
       * @param options Additional options to pass into mount
       */
      mount(
        component: React.ReactNode,
        options?: MountOptions
      ): Cypress.Chainable<MountReturn>;

      /** Mounts the component wrapped by all the providers:
       * QueryClientProvider, ErrorBoundary, Suspense, BrowserRouter
       * @param component React Node to mount
       * @param options Additional options to pass into mount
       */
      wrappedMount(
        component: React.ReactNode,
        options?: MountOptions
      ): Cypress.Chainable<MountReturn>;

      /** Visits heroes or villains routes, uses real network, verifies path */
      visitEntities(entityRoute: EntityRoute): Cypress.Chainable<string>;

      /** Visits heroes or villains routes, uses stubbed network, verifies path */
      visitStubbedEntities(entityRoute: EntityRoute): Cypress.Chainable<string>;

      /**
       * Gets an entity by name.
       * ```js
       * cy.getEntityByName(newHero.name).then(myHero => ...)
       * ```
       * @param name: Hero['name']
       */
      getEntityByProperty(
        entityType: EntityType,
        property: HeroProperty | VillainProperty
      ): Cypress.Chainable<Hero | Villain | Boy>;

      /**
       * Given a hero property (name, description or id),
       * returns the index of the hero, and the entire collection, as an object.
       */
      findEntityIndex(
        entityType: EntityType,
        property: HeroProperty
      ): Cypress.Chainable<{
        entityIndex: number;
        entityArray: Hero[] | Villain[] | Boy[];
      }>;

      /**
       * Performs crud operations GET, POST, PUT and DELETE.
       *
       * `body` and `allowedToFail are optional.
       *
       * If they are not passed in, body is empty but `allowedToFail` still is `false`.
       *
       * If the body is passed in and the method is `POST` or `PUT`, the payload will be taken,
       * otherwise undefined for `GET` and `DELETE`.
       * @param method
       * @param route
       * @param options: {body?: Hero | object; allowedToFail?: boolean}
       */
      crud(
        method: "GET" | "POST" | "PUT" | "DELETE",
        route: string,
        {
          body,
          allowedToFail = false,
        }: { body?: Hero | object; allowedToFail?: boolean } = {}
      ): Cypress.Chainable<Response<Hero[] & Hero>>;

      /**
       * Resets the data in the database to the initial data.
       */
      resetData(): Cypress.Chainable<
        Response<(Hero[] & Hero) | (Villain[] & Villain) | (Boy[] & Boy)>
      >;
    }
  }
}
````

## Kahramanların boys aynasını oluşturun

### `./db.json`'i güncelleyin

`boys` grubunu ekleyin.

```json
{
  "heroes": [
    {
      "id": "HeroAslaug",
      "name": "Aslaug",
      "description": "warrior queen"
    },
    {
      "id": "HeroBjorn",
      "name": "Bjorn Ironside",
      "description": "king of 9th century Sweden"
    },
    {
      "id": "HeroIvar",
      "name": "Ivar the Boneless",
      "description": "commander of the Great Heathen Army"
    },
    {
      "id": "HeroLagertha",
      "name": "Lagertha the Shieldmaiden",
      "description": "aka Hlaðgerðr"
    },
    {
      "id": "HeroRagnar",
      "name": "Ragnar Lothbrok",
      "description": "aka Ragnar Sigurdsson"
    },
    {
      "id": "HeroThora",
      "name": "Thora Town-hart",
      "description": "daughter of Earl Herrauðr of Götaland"
    }
  ],
  "villains": [
    {
      "id": "VillainMadelyn",
      "name": "Madelyn",
      "description": "the cat whisperer"
    },
    {
      "id": "VillainHaley",
      "name": "Haley",
      "description": "pen wielder"
    },
    {
      "id": "VillainElla",
      "name": "Ella",
      "description": "fashionista"
    },
    {
      "id": "VillainLandon",
      "name": "Landon",
      "description": "Mandalorian mauler"
    }
  ],
  "boys": [
    {
      "id": "BoyHomelander",
      "name": "Homelander",
      "description": "Like Superman, but a jerk."
    },
    {
      "id": "BoyAnnieJanuary",
      "name": "Annie January",
      "description": "The Defender of Des Moines."
    },
    {
      "id": "BoyBillyButcher",
      "name": "Billy Butcher",
      "description": "A former member of the British special forces turned vigilante."
    },
    {
      "id": "BoyBlackNoir",
      "name": "Black Noir",
      "description": "Master Martial Artist, expert hand-to-hand combatant highly trained in various forms of martial arts."
    }
  ]
}
```

Veritabanı durumunu doğru şekilde sıfırlayabilmemiz için bunu `./cypress/fixtures/db.json`'a yansıtın.

```json
{
  "heroes": [
    {
      "id": "HeroAslaug",
      "name": "Aslaug",
      "description": "warrior queen"
    },
    {
      "id": "HeroBjorn",
      "name": "Bjorn Ironside",
      "description": "king of 9th century Sweden"
    },
    {
      "id": "HeroIvar",
      "name": "Ivar the Boneless",
      "description": "commander of the Great Heathen Army"
    },
    {
      "id": "HeroLagertha",
      "name": "Lagertha the Shieldmaiden",
      "description": "aka Hlaðgerðr"
    },
    {
      "id": "HeroRagnar",
      "name": "Ragnar Lothbrok",
      "description": "aka Ragnar Sigurdsson"
    },
    {
      "id": "HeroThora",
      "name": "Thora Town-hart",
      "description": "daughter of Earl Herrauðr of Götaland"
    }
  ],
  "villains": [
    {
      "id": "VillainMadelyn",
      "name": "Madelyn",
      "description": "the cat whisperer"
    },
    {
      "id": "VillainHaley",
      "name": "Haley",
      "description": "pen wielder"
    },
    {
      "id": "VillainElla",
      "name": "Ella",
      "description": "fashionista"
    },
    {
      "id": "VillainLandon",
      "name": "Landon",
      "description": "Mandalorian mauler"
    }
  ],
  "boys": [
    {
      "id": "BoyHomelander",
      "name": "Homelander",
      "description": "Like Superman, but a jerk."
    },
    {
      "id": "BoyAnnieJanuary",
      "name": "Annie January",
      "description": "The Defender of Des Moines."
    },
    {
      "id": "BoyBillyButcher",
      "name": "Billy Butcher",
      "description": "A former member of the British special forces turned vigilante."
    },
    {
      "id": "BoyBlackNoir",
      "name": "Black Noir",
      "description": "Master Martial Artist, expert hand-to-hand combatant highly trained in various forms of martial arts."
    }
  ]
}
```

### `./cypress/fixtures/boys.json` eklentisini ekleyin.

```json
[
  {
    "id": "BoyHomelander",
    "name": "Homelander",
    "description": "Like Superman, but a jerk."
  },
  {
    "id": "BoyAnnieJanuary",
    "name": "Annie January",
    "description": "The Defender of Des Moines."
  },
  {
    "id": "BoyBillyButcher",
    "name": "Billy Butcher",
    "description": "A former member of the British special forces turned vigilante."
  },
  {
    "id": "BoyBlackNoir",
    "name": "Black Noir",
    "description": "Master Martial Artist, expert hand-to-hand combatant highly trained in various forms of martial arts."
  }
]
```

### Bileşenleri yansıtın

Kahramanlar grubunun olduğu gibi boys için 3 bileşen oluşturuyoruz. Ayrıca bazı temel bileşenleri güncellememiz gerekiyor.

`ListHeader` için yalnızca `title` için tip değişir.

```tsx
// src/components/ListHeader.tsx
import { MouseEvent } from "react";
import { NavLink } from "react-router-dom";
import { FiRefreshCcw } from "react-icons/fi";
import { GrAdd } from "react-icons/gr";

type ListHeaderProps = {
  title: "Heroes" | "Villains" | "Boys" | "About";
  handleAdd: (e: MouseEvent<HTMLButtonElement>) => void;
  handleRefresh: (e: MouseEvent<HTMLButtonElement>) => void;
};

export default function ListHeader({
  title,
  handleAdd,
  handleRefresh,
}: ListHeaderProps) {
  return (
    <div data-cy="list-header" className="content-title-group">
      <NavLink data-cy="title" to={title}>
        <h2>{title}</h2>
      </NavLink>
      <button data-cy="add-button" onClick={handleAdd} aria-label="add">
        <GrAdd />
      </button>
      <button
        data-cy="refresh-button"
        onClick={handleRefresh}
        aria-label="refresh"
      >
        <FiRefreshCcw />
      </button>
    </div>
  );
}
```

Diğerleri aynalardır. `./src/boys/` altında bir klasör ve onun altında 3 ayna dosyası oluşturun.

```tsx
// src/boys/BoyDetail.tsx
import { useState, ChangeEvent } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { FaUndo, FaRegSave } from "react-icons/fa";
import InputDetail from "components/InputDetail";
import ButtonFooter from "components/ButtonFooter";
import PageSpinner from "components/PageSpinner";
import ErrorComp from "components/ErrorComp";
import { useEntityParams } from "hooks/useEntityParams";
import { usePostEntity } from "hooks/usePostEntity";
import { Boy } from "models/Boy";
import { usePutEntity } from "hooks/usePutEntity";

export default function BoyDetail() {
  const { id } = useParams();
  const { name, description } = useEntityParams();
  const [boy, setBoy] = useState({ id, name, description });
  const { mutate: createBoy, status, error: postError } = usePostEntity("boy");
  const {
    updateEntity: updateBoy,
    isUpdating,
    isUpdateError,
  } = usePutEntity("boy");

  const navigate = useNavigate();
  const handleCancel = () => navigate("/boys");
  const handleSave = () =>
    name ? updateBoy(boy as Boy) : createBoy(boy as Boy);
  const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
    setBoy({ ...boy, name: e.target.value });
  };
  const handleDescriptionChange = (e: ChangeEvent<HTMLInputElement>) => {
    setBoy({ ...boy, description: e.target.value });
  };

  if (status === "loading" || isUpdating) {
    return <PageSpinner />;
  }

  if (postError || isUpdateError) {
    return <ErrorComp />;
  }

  return (
    <div data-cy="boy-detail" className="card edit-detail">
      <header className="card-header">
        <p className="card-header-title">{name}</p>
        &nbsp;
      </header>
      <div className="card-content">
        <div className="content">
          {id && (
            <InputDetail name={"id"} value={id} readOnly={true}></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={name ? name : ""}
            placeholder="e.g. Colleen"
            onChange={handleNameChange}
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={description ? description : ""}
            placeholder="e.g. dance fight!"
            onChange={handleDescriptionChange}
          ></InputDetail>
        </div>
      </div>
      <footer className="card-footer">
        <ButtonFooter
          label="Cancel"
          IconClass={FaUndo}
          onClick={handleCancel}
        />
        <ButtonFooter label="Save" IconClass={FaRegSave} onClick={handleSave} />
      </footer>
    </div>
  );
}
```

```tsx
// src/boys/BoyList.tsx
import { useNavigate } from "react-router-dom";
import CardContent from "components/CardContent";
import ButtonFooter from "components/ButtonFooter";
import { FaEdit, FaRegSave } from "react-icons/fa";
import {
  ChangeEvent,
  MouseEvent,
  useTransition,
  useEffect,
  useState,
  useDeferredValue,
} from "react";
import { Boy } from "models/Boy";
import { BoyProperty } from "models/types";

type BoyListProps = {
  boys: Boy[];
  handleDeleteBoy: (boy: Boy) => (e: MouseEvent<HTMLButtonElement>) => void;
};

export default function BoyList({ boys, handleDeleteBoy }: BoyListProps) {
  const deferredBoys = useDeferredValue(boys);
  const isStale = deferredBoys !== boys;
  const [filteredBoys, setFilteredBoys] = useState(deferredBoys);
  const navigate = useNavigate();
  const [isPending, startTransition] = useTransition();

  // needed to refresh the list after deleting a boy
  useEffect(() => setFilteredBoys(deferredBoys), [deferredBoys]);

  // currying: the outer fn takes our custom arg and returns a fn that takes the event
  const handleSelectBoy = (boyId: string) => () => {
    const boy = deferredBoys.find((b: Boy) => b.id === boyId);
    navigate(
      `/boys/edit-boy/${boy?.id}?name=${boy?.name}&description=${boy?.description}`
    );
  };

  /** returns a boolean whether the boy properties exist in the search field */
  const searchExists = (searchProperty: BoyProperty, searchField: string) =>
    String(searchProperty).toLowerCase().indexOf(searchField.toLowerCase()) !==
    -1;

  /** given the data and the search field, returns the data in which the search field exists */
  const searchProperties = (searchField: string, data: Boy[]) =>
    [...data].filter((item: Boy) =>
      Object.values(item).find((property: BoyProperty) =>
        searchExists(property, searchField)
      )
    );

  /** filters the boys data to see if the any of the properties exist in the list */
  const handleSearch =
    (data: Boy[]) => (event: ChangeEvent<HTMLInputElement>) => {
      const searchField = event.target.value;

      return startTransition(() =>
        setFilteredBoys(searchProperties(searchField, data))
      );
    };

  return (
    <div
      style={{
        opacity: isPending ? 0.5 : 1,
        color: isStale ? "dimgray" : "black",
      }}
    >
      {deferredBoys.length > 0 && (
        <div className="card-content">
          <span>Search </span>
          <input data-cy="search" onChange={handleSearch(deferredBoys)} />
        </div>
      )}
      &nbsp;
      <ul data-cy="boy-list" className="list">
        {filteredBoys.map((boy, index) => (
          <li data-cy={`boy-list-item-${index}`} key={boy.id}>
            <div className="card">
              <CardContent name={boy.name} description={boy.description} />
              <footer className="card-footer">
                <ButtonFooter
                  label="Delete"
                  IconClass={FaRegSave}
                  onClick={handleDeleteBoy(boy)}
                />
                <ButtonFooter
                  label="Edit"
                  IconClass={FaEdit}
                  onClick={handleSelectBoy(boy.id)}
                />
              </footer>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

```tsx
// src/boys/Boys.tsx
import { useState } from "react";
import { useNavigate, Routes, Route } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import ErrorComp from "components/ErrorComp";
import BoyList from "./BoyList";
import BoyDetail from "./BoyDetail";
import { useGetEntities } from "hooks/useGetEntities";
import { useDeleteEntity } from "hooks/useDeleteEntity";
import { Boy } from "models/Boy";

export default function Boys() {
  const [showModal, setShowModal] = useState<boolean>(false);
  const { entities: boys, getError } = useGetEntities("boys");
  const [boyToDelete, setBoyToDelete] = useState<Boy | null>(null);
  const { deleteEntity: deleteBoy, isDeleteError } = useDeleteEntity("boy");

  const navigate = useNavigate();
  const addNewBoy = () => navigate("/boys/add-boy");
  const handleRefresh = () => navigate("/boys");

  const handleCloseModal = () => {
    setBoyToDelete(null);
    setShowModal(false);
  };
  // currying: the outer fn takes our custom arg and returns a fn that takes the event
  const handleDeleteBoy = (boy: Boy) => () => {
    setBoyToDelete(boy);
    setShowModal(true);
  };
  const handleDeleteFromModal = () => {
    boyToDelete ? deleteBoy(boyToDelete) : null;
    setShowModal(false);
  };

  if (getError || isDeleteError) {
    return <ErrorComp />;
  }

  return (
    <div data-cy="boys">
      <ListHeader
        title="Boys"
        handleAdd={addNewBoy}
        handleRefresh={handleRefresh}
      />
      <div>
        <div>
          <Routes>
            <Route
              path=""
              element={
                <BoyList boys={boys} handleDeleteBoy={handleDeleteBoy} />
              }
            />
            <Route path="/add-boy" element={<BoyDetail />} />
            <Route path="/edit-boy/:id" element={<BoyDetail />} />
            <Route
              path="*"
              element={
                <BoyList boys={boys} handleDeleteBoy={handleDeleteBoy} />
              }
            />
          </Routes>
        </div>
      </div>

      {showModal && (
        <ModalYesNo
          message="Would you like to delete the boy?"
          onNo={handleCloseModal}
          onYes={handleDeleteFromModal}
        />
      )}
    </div>
  );
}
```

Boys rotasını `App.tsx`'e ekleyin.

```tsx
// src/App.tsx
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "react-query";
import { ErrorBoundary } from "react-error-boundary";
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import PageSpinner from "components/PageSpinner";
import ErrorComp from "components/ErrorComp";
import Villains from "villains/Villains";
import Boys from "boys/Boys";
import "./styles.scss";
const Heroes = lazy(() => import("heroes/Heroes"));
const NotFound = lazy(() => import("components/NotFound"));
const About = lazy(() => import("About"));

const queryClient = new QueryClient();

export default function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
        <main className="column">
          <QueryClientProvider client={queryClient}>
            <ErrorBoundary fallback={<ErrorComp />}>
              <Suspense fallback={<PageSpinner />}>
                <Routes>
                  <Route path="/" element={<Navigate replace to="/heroes" />} />
                  <Route path="/heroes/*" element={<Heroes />} />
                  <Route path="/villains/*" element={<Villains />} />
                  <Route path="/boys/*" element={<Boys />} />
                  <Route path="/about" element={<About />} />
                  <Route path="*" element={<NotFound />} />
                </Routes>
              </Suspense>
            </ErrorBoundary>
          </QueryClientProvider>
        </main>
      </div>
    </BrowserRouter>
  );
}
```

`Boys`'u `NavBar` bileşenine ekleyin.

```tsx
// src/components/NavBar.tsx
import { NavLink } from "react-router-dom";

export default function NavBar() {
  const linkIsActive = (link: { isActive: boolean }) =>
    link.isActive ? "active-link" : "";

  return (
    <nav data-cy="nav-bar" className="column is-2 menu">
      <p className="menu-label">Menu</p>
      <ul data-cy="menu-list" className="menu-list">
        <NavLink to="/heroes" className={linkIsActive}>
          Heroes
        </NavLink>
        <NavLink to="/villains" className={linkIsActive}>
          Villains
        </NavLink>
        <NavLink to="/boys" className={linkIsActive}>
          Boys
        </NavLink>
        <NavLink to="/about" className={linkIsActive}>
          About
        </NavLink>
      </ul>
    </nav>
  );
}
```

### E2E testlerinin aynasını oluşturun

Kahraman sürümlerinin boys aynaları olan 3 yeni e2e testi oluşturuyoruz.

```typescript
// cypress/e2e/create-boy.cy.ts
import { faker } from "@faker-js/faker";
describe("Create boy", () => {
  before(cy.resetData);

  const navToAddBoy = () => {
    cy.location("pathname").should("eq", "/boys");
    cy.getByCy("add-button").click();
    cy.location("pathname").should("eq", "/boys/add-boy");
    cy.getByCy("boy-detail").should("be.visible");
    cy.getByCy("input-detail-id").should("not.exist");
  };

  it("should go through the refresh flow (ui-integration)", () => {
    cy.visitStubbedEntities("boys");
    navToAddBoy();

    cy.getByCy("refresh-button").click();
    cy.location("pathname").should("eq", "/boys");
    cy.getByCy("boy-list").should("be.visible");
  });

  it("should go through the cancel flow and perform direct navigation (ui-integration)", () => {
    cy.intercept("GET", `${Cypress.env("API_URL")}/boys`, {
      fixture: "boys",
    }).as("stubbedGetBoys");
    cy.visit("/boys/add-boy");
    cy.wait("@stubbedGetBoys");

    cy.getByCy("cancel-button").click();
    cy.location("pathname").should("eq", "/boys");
    cy.getByCy("boy-list").should("be.visible");
  });

  it("should go through the add boy flow (ui-e2e)", () => {
    cy.visitEntities("boys");
    navToAddBoy();

    const newBoy = {
      name: faker.internet.userName(),
      description: `description ${faker.internet.userName()}`,
    };
    cy.getByCy("input-detail-name").type(newBoy.name);
    cy.getByCy("input-detail-description").type(newBoy.description);
    cy.getByCy("save-button").click();

    cy.location("pathname").should("eq", "/boys");

    cy.getByCy("boys").should("be.visible");
    cy.getByCyLike("boy-list-item").should("have.length.gt", 0);
    cy.getByCy("boy-list")
      .should("contain", newBoy.name)
      .and("contain", newBoy.description);

    cy.getEntityByProperty("boy", newBoy.name).then((myBoy) =>
      cy.crud("DELETE", `boys/${myBoy.id}`)
    );
  });
});
```

```typescript
// cypress/e2e/delete-boy.cy.ts
import { faker } from "@faker-js/faker";
import { Boy } from "../../src/models/Boy";
describe("Delete boy", () => {
  before(cy.resetData);

  const yesOnModal = () =>
    cy.getByCy("modal-yes-no").within(() => cy.getByCy("button-yes").click());

  it("should go through the cancel flow (ui-integration)", () => {
    cy.visitStubbedEntities("boys");

    cy.getByCy("delete-button").first().click();
    cy.getByCy("modal-yes-no").within(() => cy.getByCy("button-no").click());
    cy.getByCy("boys").should("be.visible");
    cy.get("modal-yes-no").should("not.exist");
  });

  it("should go through the edit flow (ui-e2e)", () => {
    const boy: Boy = {
      id: faker.datatype.uuid(),
      name: faker.internet.userName(),
      description: `description ${faker.internet.userName()}`,
    };

    cy.crud("POST", "boys", { body: boy });

    cy.visitEntities("boys");

    cy.findEntityIndex("boy", boy.id).then(
      ({ entityIndex: boyIndex, entityArray: boyArray }) => {
        cy.getByCy("delete-button").eq(boyIndex).click();

        yesOnModal();

        cy.getByCy("boy-list")
          .should("be.visible")
          .should("not.contain", boyArray[boyIndex].name)
          .and("not.contain", boyArray[boyIndex].description);
      }
    );
  });
});
```

```typescript
// cypress/e2e/edit-boy.cy.ts
import { faker } from "@faker-js/faker";
import { Boy } from "../../src/models/Boy";
describe("Edit boy", () => {
  before(cy.resetData);

  /** Verifies boy info on Edit page */
  const verifyBoy = (boys: Boy[], heroIndex: number) => {
    cy.location("pathname").should("include", "/boys/edit-boy/");
    cy.getByCy("boy-detail").should("be.visible");
    cy.getByCy("input-detail-id").should("be.visible");
    cy.findByDisplayValue(boys[heroIndex].id);
    cy.findByDisplayValue(boys[heroIndex].name);
    cy.findByDisplayValue(boys[heroIndex].description);
  };

  const randomBoyIndex = (boys: Boy[]) => Cypress._.random(0, boys.length - 1);

  it("should go through the cancel flow for a random boy (ui-integration)", () => {
    cy.visitStubbedEntities("boys");

    cy.fixture("boys").then((boys) => {
      const heroIndex = randomBoyIndex(boys);
      cy.getByCy("edit-button").eq(heroIndex).click();
      verifyBoy(boys, heroIndex);
    });

    cy.getByCy("cancel-button").click();
    cy.location("pathname").should("eq", "/boys");
    cy.getByCy("boy-list").should("be.visible");
  });

  it("should go through the PUT error flow (ui-integration)", () => {
    cy.visitStubbedEntities("boys");

    cy.fixture("boys").then((boys) => {
      const heroIndex = randomBoyIndex(boys);
      cy.getByCy("edit-button").eq(heroIndex).click();
      verifyBoy(boys, heroIndex);
    });

    cy.intercept("PUT", `${Cypress.env("API_URL")}/boys/*`, {
      statusCode: 500,
      delay: 100,
    }).as("isUpdateError");

    cy.getByCy("save-button").click();
    cy.getByCy("spinner");
    cy.wait("@isUpdateError");
    cy.getByCy("error");
  });

  it("should navigate to add from an existing boy (ui-integration)", () => {
    cy.visitStubbedEntities("boys");

    cy.fixture("boys").then((boys) => {
      const heroIndex = randomBoyIndex(boys);
      cy.getByCy("edit-button").eq(heroIndex).click();
      verifyBoy(boys, heroIndex);

      cy.getByCy("add-button").click();
      cy.getByCy("input-detail-id").should("not.exist");
      cy.findByDisplayValue(boys[heroIndex].name).should("not.exist");
      cy.findByDisplayValue(boys[heroIndex].description).should("not.exist");
    });
  });

  it("should go through the edit flow (ui-e2e)", () => {
    const newBoy: Boy = {
      id: faker.datatype.uuid(),
      name: faker.internet.userName(),
      description: `description ${faker.internet.userName()}`,
    };

    cy.crud("POST", "boys", { body: newBoy });

    cy.visit(`boys/edit-boy/${newBoy.id}`, {
      qs: { name: newBoy.name, description: newBoy.description },
    });

    const editedBoy = {
      name: faker.internet.userName(),
      description: `description ${faker.internet.userName()}`,
    };

    cy.getByCy("input-detail-name")
      .find(".input")
      .clear()
      .type(`${editedBoy.name}`);
    cy.getByCy("input-detail-description")
      .find(".input")
      .clear()
      .type(`${editedBoy.description}`);
    cy.getByCy("save-button").click();

    cy.getByCy("boy-list")
      .should("be.visible")
      .should("contain", editedBoy.name)
      .and("contain", editedBoy.description);

    cy.getEntityByProperty("boy", newBoy.id).then((myBoy: Boy) =>
      cy.crud("DELETE", `boys/${myBoy.id}`)
    );
  });
});
```

Arka uç testi, kötü adamları kapsayacak yeni bir bloğa ihtiyaç duyar.

```typescript
// cypress/e2e/backend/crud.cy.ts
import { faker } from "@faker-js/faker";
import { Hero } from "../../../src/models/Hero";
import { Villain } from "../../../src/models/Villain";
import { Boy } from "../../../src/models/Boy";

describe("Backend e2e", () => {
  const assertProperties = (entity: Hero | Villain | Boy) => {
    expect(entity.id).to.be.a("string");
    expect(entity.name).to.be.a("string");
    expect(entity.description).to.be.a("string");
  };

  before(() => cy.resetData());

  it("should GET heroes and villains ", () => {
    cy.crud("GET", "heroes")
      .its("body")
      .should("have.length.gt", 0)
      .each(assertProperties);

    cy.crud("GET", "villains")
      .its("body")
      .should("have.length.gt", 0)
      .each(assertProperties);
  });

  it("should CRUD a new hero entity", () => {
    const newHero = {
      id: faker.datatype.uuid(),
      name: faker.internet.userName(),
      description: `description ${faker.internet.userName()}`,
    };

    cy.crud("POST", "heroes", { body: newHero })
      .its("status")
      .should("eq", 201);

    cy.crud("GET", "heroes")
      .its("body")
      .then((body) => {
        expect(body.at(-1)).to.deep.eq(newHero);
      });

    const editedHero = { ...newHero, name: "Murat" };
    cy.crud("PUT", `heroes/${editedHero.id}`, { body: editedHero })
      .its("status")
      .should("eq", 200);
    cy.crud("GET", `heroes/${editedHero.id}`)
      .its("body")
      .should("deep.eq", editedHero);

    cy.crud("DELETE", `heroes/${editedHero.id}`)
      .its("status")
      .should("eq", 200);
    cy.crud("GET", `heroes/${editedHero.id}`, { allowedToFail: true })
      .its("status")
      .should("eq", 404);
  });

  it("should CRUD a new villain entity", () => {
    const newVillain = {
      id: faker.datatype.uuid(),
      name: faker.internet.userName(),
      description: `description ${faker.internet.userName()}`,
    };

    cy.crud("POST", "villains", { body: newVillain })
      .its("status")
      .should("eq", 201);

    cy.crud("GET", "villains")
      .its("body")
      .then((body) => {
        expect(body.at(-1)).to.deep.eq(newVillain);
      });

    const editedVillain = { ...newVillain, name: "Murat" };
    cy.crud("PUT", `villains/${editedVillain.id}`, { body: editedVillain })
      .its("status")
      .should("eq", 200);
    cy.crud("GET", `villains/${editedVillain.id}`)
      .its("body")
      .should("deep.eq", editedVillain);

    cy.crud("DELETE", `villains/${editedVillain.id}`)
      .its("status")
      .should("eq", 200);
    cy.crud("GET", `villains/${editedVillain.id}`, { allowedToFail: true })
      .its("status")
      .should("eq", 404);
  });

  it("should CRUD a new boy entity", () => {
    const newVillain = {
      id: faker.datatype.uuid(),
      name: faker.internet.userName(),
      description: `description ${faker.internet.userName()}`,
    };

    cy.crud("POST", "boys", { body: newVillain })
      .its("status")
      .should("eq", 201);

    cy.crud("GET", "boys")
      .its("body")
      .then((body) => {
        expect(body.at(-1)).to.deep.eq(newVillain);
      });

    const editedVillain = { ...newVillain, name: "Murat" };
    cy.crud("PUT", `boys/${editedVillain.id}`, { body: editedVillain })
      .its("status")
      .should("eq", 200);
    cy.crud("GET", `boys/${editedVillain.id}`)
      .its("body")
      .should("deep.eq", editedVillain);

    cy.crud("DELETE", `boys/${editedVillain.id}`)
      .its("status")
      .should("eq", 200);
    cy.crud("GET", `boys/${editedVillain.id}`, { allowedToFail: true })
      .its("status")
      .should("eq", 404);
  });
});
```

routes-nav, kötü adamlar rotasını kapsayacak yeni bir teste ihtiyaç duyar.

```typescript
// cypress/e2e/routes-nav.cy.ts
describe("routes navigation (ui-integration)", () => {
  beforeEach(() => {
    cy.intercept("GET", `${Cypress.env("API_URL")}/heroes`, {
      fixture: "heroes",
    }).as("stubbedGetHeroes");
  });
  it("should land on baseUrl, redirect to /heroes", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");

    cy.location("pathname").should("eq", "/heroes");
    cy.getByCy("heroes").should("be.visible");
  });

  it("should direct-navigate to /heroes", () => {
    const route = "/heroes";
    cy.visit(route);
    cy.location("pathname").should("eq", route);
    cy.getByCy("heroes").should("be.visible");
  });

  it("should direct-navigate to /villains", () => {
    const route = "/villains";
    cy.visit(route);
    cy.location("pathname").should("eq", route);
    cy.getByCy("villains").should("be.visible");
  });

  it("should land on not found when visiting an non-existing route", () => {
    const route = "/route48";
    cy.visit(route);
    cy.location("pathname").should("eq", route);
    cy.getByCy("not-found").should("be.visible");
  });

  it("should direct-navigate to about", () => {
    const route = "/about";
    cy.visit(route);
    cy.location("pathname").should("eq", route);
    cy.getByCy("about").contains("CCTDD");
  });

  it("should cover route history with browser back and forward", () => {
    cy.visit("/about");
    const routes = ["villains", "heroes", "about"];
    cy.wrap(routes).each((route: string) =>
      cy.get(`[href="/${route}"]`).click()
    );

    const lastIndex = routes.length - 1;
    cy.location("pathname").should("include", routes[lastIndex]);
    cy.go("back");
    cy.location("pathname").should("include", routes[lastIndex - 1]);
    cy.go("back");
    cy.location("pathname").should("include", routes[lastIndex - 2]);
    cy.go("forward").go("forward");
    cy.location("pathname").should("include", routes[lastIndex]);
  });
});
```

### Cypress bileşen testlerini yansıtın

Öncelikle, `Navbar` ve `App` testlerini güncelleyin. `Navbar.cy` için, `routes` dizisine `boys` eklememiz yeterlidir.

```tsx
// src/components/NavBar.cy.tsx
import NavBar from "./NavBar";
import { BrowserRouter } from "react-router-dom";
import "../styles.scss";

describe("NavBar", () => {
  it("should navigate to the correct routes", () => {
    cy.mount(
      <BrowserRouter>
        <NavBar />
      </BrowserRouter>
    );

    cy.contains("p", "Menu");
    cy.getByCy("menu-list").children().should("have.length", routes.length);

    routes.forEach((route: string) => {
      cy.get(`[href="/${route}"]`)
        .contains(route, { matchCase: false })
        .click()
        .should("have.class", "active-link")
        .siblings()
        .should("not.have.class", "active-link");

      cy.url().should("contain", route);
    });
  });
});
```

`App.cy` için, `boys` rotasını engellememiz ve `boys` için bir kontrol eklememiz gerekmektedir.

```tsx
// src/App.cy.tsx
import App from "./App";

describe("ct sanity", () => {
  it("should render the App", () => {
    cy.intercept("GET", `${Cypress.env("API_URL")}/heroes`, {
      fixture: "heroes.json",
    }).as("getHeroes");

    cy.intercept("GET", `${Cypress.env("API_URL")}/villains`, {
      fixture: "villains.json",
    }).as("getVillains");

    cy.intercept("GET", `${Cypress.env("API_URL")}/boys`, {
      fixture: "boys.json",
    });

    cy.mount(<App />);
    cy.getByCy("not-found").should("be.visible");

    cy.contains("Heroes").click();
    cy.getByCy("heroes").should("be.visible");

    cy.contains("Villains").click();
    cy.getByCy("villains").should("be.visible");

    cy.contains("Boys").click();
    cy.getByCy("boys").should("be.visible");

    cy.contains("About").click();
    cy.getByCy("about").should("be.visible");
  });
});
```

`boys` altındaki 3 bileşen için 3 yeni bileşen testine ihtiyacımız var.

```tsx
// src/boys/BoyDetail.cy.tsx
import BoyDetail from "./BoyDetail";
import "../styles.scss";
import React from "react";
import * as postHook from "hooks/usePostEntity";

describe("BoyDetail", () => {
  beforeEach(() => {
    cy.wrappedMount(<BoyDetail />);
  });

  it("should handle Save", () => {
    // example of testing implementation details
    cy.spy(React, "useState").as("useState");
    cy.spy(postHook, "usePostEntity").as("usePostEntity");
    // instead prefer to test at a higher level
    cy.intercept("POST", "*", { statusCode: 200 }).as("postBoy");
    cy.getByCy("save-button").click();

    // test at a higher level
    cy.wait("@postBoy");
    // test implementation details (what not to do)
    cy.get("@useState").should("have.been.called");
    cy.get("@usePostEntity").should("have.been.called");
  });

  it("should handle non-200 Save", () => {
    cy.intercept("POST", "*", { statusCode: 400, delay: 100 }).as("postBoy");
    cy.getByCy("save-button").click();
    cy.getByCy("spinner");
    cy.wait("@postBoy");
    cy.getByCy("error");
  });

  it("should handle Cancel", () => {
    cy.getByCy("cancel-button").click();
    cy.location("pathname").should("eq", "/boys");
  });

  it("should handle name change", () => {
    const newBoyName = "abc";
    cy.getByCy("input-detail-name").type(newBoyName);

    cy.findByDisplayValue(newBoyName).should("be.visible");
  });

  it("should handle description change", () => {
    const newBoyDescription = "123";
    cy.getByCy("input-detail-description").type(newBoyDescription);

    cy.findByDisplayValue(newBoyDescription).should("be.visible");
  });

  it("id: false, name: false - should verify the minimal state of the component", () => {
    cy.get("p").then(($el) => cy.wrap($el.text()).should("equal", ""));
    cy.getByCyLike("input-detail").should("have.length", 2);
    cy.getByCy("input-detail-id").should("not.exist");

    cy.findByPlaceholderText("e.g. Colleen").should("be.visible");
    cy.findByPlaceholderText("e.g. dance fight!").should("be.visible");

    cy.getByCy("save-button").should("be.visible");
    cy.getByCy("cancel-button").should("be.visible");
  });
});
```

```tsx
// src/boys/BoyList.cy.tsx
import BoyList from "./BoyList";
import "../styles.scss";
import boys from "../../cypress/fixtures/boys.json";

describe("BoyList", () => {
  it("no boys should not display a list nor search bar", () => {
    cy.wrappedMount(
      <BoyList boys={[]} handleDeleteBoy={cy.stub().as("handleDeleteBoy")} />
    );

    cy.getByCy("boy-list").should("exist");
    cy.getByCyLike("boy-list-item").should("not.exist");
    cy.getByCy("search").should("not.exist");
  });

  context("with boys in the list", () => {
    beforeEach(() => {
      cy.wrappedMount(
        <BoyList
          boys={boys}
          handleDeleteBoy={cy.stub().as("handleDeleteBoy")}
        />
      );
    });

    it("should render the boy layout", () => {
      cy.getByCyLike("boy-list-item").should("have.length", boys.length);

      cy.getByCy("card-content");
      cy.contains(boys[0].name);
      cy.contains(boys[0].description);

      cy.get("footer")
        .first()
        .within(() => {
          cy.getByCy("delete-button");
          cy.getByCy("edit-button");
        });
    });

    it("should search and filter boy by name and description", () => {
      cy.getByCy("search").type(boys[0].name);
      cy.getByCyLike("boy-list-item")
        .should("have.length", 1)
        .contains(boys[0].name);

      cy.getByCy("search").clear().type(boys[2].description);
      cy.getByCyLike("boy-list-item")
        .should("have.length", 1)
        .contains(boys[2].description);
    });

    it("should handle delete", () => {
      cy.getByCy("delete-button").first().click();
      cy.get("@handleDeleteBoy").should("have.been.called");
    });

    it("should handle edit", () => {
      cy.getByCy("edit-button").first().click();
      cy.location("pathname").should("eq", "/boys/edit-boy/" + boys[0].id);
    });
  });
});
```

```tsx
// src/boys/Boys.cy.tsx
import Boys from "./Boys";
import "../styles.scss";

describe("Boys", () => {
  it("should see error on initial load with GET", () => {
    Cypress.on("uncaught:exception", () => false);
    cy.clock();
    cy.intercept("GET", `${Cypress.env("API_URL")}/boys`, {
      statusCode: 400,
      delay: 100,
    }).as("notFound");

    cy.wrappedMount(<Boys />);

    cy.getByCy("page-spinner").should("be.visible");
    Cypress._.times(3, () => {
      cy.tick(5000);
      cy.wait("@notFound");
    });
    cy.tick(5000);

    cy.getByCy("error");
  });

  context("200 flows", () => {
    beforeEach(() => {
      cy.intercept("GET", `${Cypress.env("API_URL")}/boys`, {
        fixture: "boys.json",
      }).as("getBoys");

      cy.wrappedMount(<Boys />);
    });

    it("should display the boy list on render, and go through boy add & refresh flow", () => {
      cy.wait("@getBoys");

      cy.getByCy("list-header").should("be.visible");
      cy.getByCy("boy-list").should("be.visible");

      cy.getByCy("add-button").click();
      cy.location("pathname").should("eq", "/boys/add-boy");

      cy.getByCy("refresh-button").click();
      cy.location("pathname").should("eq", "/boys");
    });

    const invokeBoyDelete = () => {
      cy.getByCy("delete-button").first().click();
      cy.getByCy("modal-yes-no").should("be.visible");
    };
    it("should go through the modal flow, and cover error on DELETE", () => {
      cy.getByCy("modal-yes-no").should("not.exist");

      cy.log("do not delete flow");
      invokeBoyDelete();
      cy.getByCy("button-no").click();
      cy.getByCy("modal-yes-no").should("not.exist");

      cy.log("delete flow");
      invokeBoyDelete();
      cy.intercept("DELETE", "*", { statusCode: 500 }).as("deleteBoy");

      cy.getByCy("button-yes").click();
      cy.wait("@deleteBoy");
      cy.getByCy("modal-yes-no").should("not.exist");
      cy.getByCy("error").should("be.visible");
    });
  });
});
```

### RTL testlerini yansıtın

Cy CT ile yapılanın aynısını RTL'ye uygulamamız yeterlidir.

```tsx
// src/boys/BoyDetail.test.tsx
import BoyDetail from "./BoyDetail";
import "@testing-library/jest-dom";
import { wrappedRender, act, screen, waitFor } from "test-utils";
import userEvent from "@testing-library/user-event";

describe("BoyDetail", () => {
  beforeEach(() => {
    wrappedRender(<BoyDetail />);
  });

  // with msw, it is not recommended to use verify XHR calls going out of the app
  // instead, the advice is the verify the changes in the UI
  // alas, sometimes there are no changes in the component itself
  // therefore we cannot test everything 1:1 versus Cypress component test
  // should handle Save and should handle non-200 Save have no RTL mirrors

  it("should handle Cancel", async () => {
    // code that causes React state updates (ex: BrowserRouter)
    // should be wrapped into act(...):
    // userEvent.click(await screen.findByTestId('cancel-button')) // won't work
    act(() => screen.getByTestId("cancel-button").click());

    expect(window.location.pathname).toBe("/boys");
  });

  it("should handle name change", async () => {
    const newBoyName = "abc";
    const inputDetailName = await screen.findByPlaceholderText("e.g. Colleen");
    userEvent.type(inputDetailName, newBoyName);

    await waitFor(async () =>
      expect(inputDetailName).toHaveDisplayValue(newBoyName)
    );
  });

  const inputDetailDescription = async () =>
    screen.findByPlaceholderText("e.g. dance fight!");

  it("should handle description change", async () => {
    const newBoyDescription = "123";

    userEvent.type(await inputDetailDescription(), newBoyDescription);
    await waitFor(async () =>
      expect(await inputDetailDescription()).toHaveDisplayValue(
        newBoyDescription
      )
    );
  });

  it("id: false, name: false - should verify the minimal state of the component", async () => {
    expect(await screen.findByTestId("input-detail-name")).toBeVisible();
    expect(await screen.findByTestId("input-detail-description")).toBeVisible();
    expect(screen.queryByTestId("input-detail-id")).not.toBeInTheDocument();

    expect(await inputDetailDescription()).toBeVisible();

    expect(await screen.findByTestId("save-button")).toBeVisible();
    expect(await screen.findByTestId("cancel-button")).toBeVisible();
  });
});
```

```tsx
// src/boys/BoyList.test.tsx
import BoyList from "./BoyList";
import { wrappedRender, screen, waitFor } from "test-utils";
import userEvent from "@testing-library/user-event";
import { boys } from "../../db.json";

describe("BoyList", () => {
  const handleDeleteBoy = jest.fn();

  it("no boys should not display a list nor search bar", async () => {
    wrappedRender(<BoyList boys={[]} handleDeleteBoy={handleDeleteBoy} />);

    expect(await screen.findByTestId("boy-list")).toBeInTheDocument();
    expect(screen.queryByTestId("boy-list-item-1")).not.toBeInTheDocument();
    expect(screen.queryByTestId("search-bar")).not.toBeInTheDocument();
  });

  describe("with boys in the list", () => {
    beforeEach(() => {
      wrappedRender(<BoyList boys={boys} handleDeleteBoy={handleDeleteBoy} />);
    });

    const cardContents = async () => screen.findAllByTestId("card-content");
    const deleteButtons = async () => screen.findAllByTestId("delete-button");
    const editButtons = async () => screen.findAllByTestId("edit-button");

    it("should render the boy layout", async () => {
      expect(
        await screen.findByTestId(`boy-list-item-${boys.length - 1}`)
      ).toBeInTheDocument();

      expect(await screen.findByText(boys[0].name)).toBeInTheDocument();
      expect(await screen.findByText(boys[0].description)).toBeInTheDocument();
      expect(await cardContents()).toHaveLength(boys.length);
      expect(await deleteButtons()).toHaveLength(boys.length);
      expect(await editButtons()).toHaveLength(boys.length);
    });

    it("should search and filter boy by name and description", async () => {
      const search = await screen.findByTestId("search");

      userEvent.type(search, boys[0].name);
      await waitFor(async () => expect(await cardContents()).toHaveLength(1));
      await screen.findByText(boys[0].name);

      userEvent.clear(search);
      await waitFor(async () =>
        expect(await cardContents()).toHaveLength(boys.length)
      );

      userEvent.type(search, boys[2].description);
      await waitFor(async () => expect(await cardContents()).toHaveLength(1));
    });

    it("should handle delete", async () => {
      userEvent.click((await deleteButtons())[0]);
      expect(handleDeleteBoy).toHaveBeenCalled();
    });

    it("should handle edit", async () => {
      userEvent.click((await editButtons())[0]);
      await waitFor(() =>
        expect(window.location.pathname).toEqual("/boys/edit-boy/" + boys[0].id)
      );
    });
  });
});
```

```tsx
// src/boys/Boys.test.tsx
import Boys from "./Boys";
import { wrappedRender, screen, waitForElementToBeRemoved } from "test-utils";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { boys } from "../../db.json";

describe("Boys", () => {
  // mute the expected console.error message, because we are mocking non-200 responses
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  jest.spyOn(console, "error").mockImplementation(() => {});

  beforeEach(() => wrappedRender(<Boys />));

  it("should see error on initial load with GET", async () => {
    const handlers = [
      rest.get(
        `${process.env.REACT_APP_API_URL}/boys`,
        async (_req, res, ctx) => res(ctx.status(400))
      ),
    ];
    const server = setupServer(...handlers);
    server.listen({
      onUnhandledRequest: "warn",
    });
    jest.useFakeTimers();

    expect(await screen.findByTestId("page-spinner")).toBeVisible();

    jest.advanceTimersByTime(25000);
    await waitForElementToBeRemoved(
      () => screen.queryByTestId("page-spinner"),
      {
        timeout: 25000,
      }
    );

    expect(await screen.findByTestId("error")).toBeVisible();
    jest.useRealTimers();
    server.resetHandlers();
    server.close();
  });

  describe("200 flows", () => {
    const handlers = [
      rest.get(
        `${process.env.REACT_APP_API_URL}/boys`,
        async (_req, res, ctx) => res(ctx.status(200), ctx.json(boys))
      ),
      rest.delete(
        `${process.env.REACT_APP_API_URL}/boys/${boys[0].id}`, // use /.*/ for all requests
        async (_req, res, ctx) =>
          res(ctx.status(400), ctx.json("expected error"))
      ),
    ];
    const server = setupServer(...handlers);
    beforeAll(() => {
      server.listen({
        onUnhandledRequest: "warn",
      });
    });
    afterEach(server.resetHandlers);
    afterAll(server.close);

    it("should display the boy list on render, and go through boy add & refresh flow", async () => {
      expect(await screen.findByTestId("list-header")).toBeVisible();
      expect(await screen.findByTestId("boy-list")).toBeVisible();

      await userEvent.click(await screen.findByTestId("add-button"));
      expect(window.location.pathname).toBe("/boys/add-boy");

      await userEvent.click(await screen.findByTestId("refresh-button"));
      expect(window.location.pathname).toBe("/boys");
    });

    const deleteButtons = async () => screen.findAllByTestId("delete-button");
    const modalYesNo = async () => screen.findByTestId("modal-yes-no");
    const maybeModalYesNo = () => screen.queryByTestId("modal-yes-no");
    const invokeBoyDelete = async () => {
      userEvent.click((await deleteButtons())[0]);
      expect(await modalYesNo()).toBeVisible();
    };

    it("should go through the modal flow, and cover error on DELETE", async () => {
      expect(screen.queryByTestId("modal-dialog")).not.toBeInTheDocument();

      await invokeBoyDelete();
      await userEvent.click(await screen.findByTestId("button-no"));
      expect(maybeModalYesNo()).not.toBeInTheDocument();

      await invokeBoyDelete();
      await userEvent.click(await screen.findByTestId("button-yes"));

      expect(maybeModalYesNo()).not.toBeInTheDocument();
      expect(await screen.findByTestId("error")).toBeVisible();
      expect(screen.queryByTestId("modal-dialog")).not.toBeInTheDocument();
    });
  });
});
```

`App.test` için bir `msw` işleyici ve yeni bir rota kontrolüne ihtiyaç duyar.

```tsx
// src/App.test.tsx
iimport {act, render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import App from './App'
import {heroes, villains, boys} from '../db.json'

import {rest} from 'msw'
import {setupServer} from 'msw/node'

describe('200 flow', () => {
  const handlers = [
    rest.get(
      `${process.env.REACT_APP_API_URL}/heroes`,
      async (_req, res, ctx) => res(ctx.status(200), ctx.json(heroes)),
    ),
    rest.get(
      `${process.env.REACT_APP_API_URL}/villains`,
      async (_req, res, ctx) => res(ctx.status(200), ctx.json(villains)),
    ),
    rest.get(`${process.env.REACT_APP_API_URL}/boys`, async (_req, res, ctx) =>
      res(ctx.status(200), ctx.json(boys)),
    ),
  ]
  const server = setupServer(...handlers)
  beforeAll(() => {
    server.listen({
      onUnhandledRequest: 'warn',
    })
  })
  afterEach(server.resetHandlers)
  afterAll(server.close)

  test('renders tour of heroes', async () => {
    render(<App />)
    await act(() => new Promise(r => setTimeout(r, 0))) // spinner

    await userEvent.click(screen.getByText('About'))
    expect(await screen.findByTestId('about')).toBeVisible()

    await userEvent.click(screen.getByText('Heroes'))
    expect(await screen.findByTestId('heroes')).toBeVisible()

    await userEvent.click(screen.getByText('Villains'))
    expect(await screen.findByTestId('villains')).toBeVisible()

    await userEvent.click(screen.getByText('Boys'))
    expect(await screen.findByTestId('boys')).toBeVisible()
  })
})
```

`Navbar.test` için `routes` değişkeninin aynı güncellemeye, yani `Boys` eklemeye ihtiyaç duyar.

```tsx
// src/components/NavBar.test.tsx
import NavBar from "./NavBar";
import { render, screen, within, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BrowserRouter } from "react-router-dom";
import "@testing-library/jest-dom";

const routes = ["Heroes", "Villains", "Boys", "About"];

describe("NavBar", () => {
  beforeEach(() => {
    render(
      <BrowserRouter>
        <NavBar />
      </BrowserRouter>
    );
  });

  it("should verify route layout", async () => {
    expect(await screen.findByText("Menu")).toBeVisible();

    const menuList = await screen.findByTestId("menu-list");
    expect(within(menuList).queryAllByRole("link").length).toBe(routes.length);

    routes.forEach((route) => within(menuList).getByText(route));
  });

  it.each(routes)("should navigate to route %s", async (route: string) => {
    const link = async (name: string) => screen.findByRole("link", { name });
    const activeRouteLink = await link(route);
    userEvent.click(activeRouteLink);
    await waitFor(() => expect(activeRouteLink).toHaveClass("active-link"));
    expect(window.location.pathname).toEqual(`/${route.toLowerCase()}`);

    const remainingRoutes = routes.filter((r) => r !== route);
    remainingRoutes.forEach(async (inActiveRoute) => {
      expect(await link(inActiveRoute)).not.toHaveClass("active-link");
    });
  });
});
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://muratkerem.gitbook.io/cctdd-tr/ch20-theboys-ramda-with-react/prerequisite.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
