ch14-Heroes-part2-react-router

Heroes bileşenine geri döndük ve bu sefer yönlendirme özelliklerine sahibiz. İlk render'da Heroes, alt HeroList bileşenini gösterir. ListHeaderdaki + düğmesine tıklayarak HeroDetaili görüntüleyebilmemiz gerekiyor. Daha sonra ListHeaderdaki yenileme düğmesine tıkladığında HeroListi tekrar görüntülememiz gerekiyor. Cancel düğmesi HeroDetailden HeroListe geri dönmelidir. Yeni bir dal oluşturun feat/Heroes-part2

Şimdilik, rotaya bağlı olarak HeroList ve HeroDetail arasında geçiş yapmak yerine, her ikisini bir arada gösterelim. Testi yazalım (Red 1).

// src/heroes/Heroes.cy.tsx
import Heroes from "./Heroes";
import { BrowserRouter } from "react-router-dom";
import "../styles.scss";

describe("Heroes", () => {
  it("should handle hero add and refresh", () => {
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.mount(
      <BrowserRouter>
        <Heroes />
      </BrowserRouter>
    );

    cy.getByCy("list-header");
    cy.getByCy("add-button").click();
    cy.get("@log").should("have.been.calledWith", "handleAdd");
    cy.getByCy("refresh-button").click();
    cy.get("@log").should("have.been.calledWith", "handleRefresh");
  });

  it.only("should display hero list on render", () => {
    cy.mount(
      <BrowserRouter>
        <Heroes />
      </BrowserRouter>
    );

    cy.getByCy("hero-list");
    cy.getByCy("hero-detail");
  });

  const invokeHeroDelete = () => {
    cy.getByCy("delete-button").first().click();
    cy.getByCy("modal-yes-no").should("be.visible");
  };
  it("should go through the modal flow", () => {
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.mount(
      <BrowserRouter>
        <Heroes />
      </BrowserRouter>
    );

    cy.getByCy("modal-yes-no").should("not.exist");

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

    cy.log("delete flow");
    invokeHeroDelete();
    cy.getByCy("button-yes").click();
    cy.getByCy("modal-yes-no").should("not.exist");
    cy.get("@log").should("have.been.calledWith", "handleDeleteFromModal");
  });
});

Testi geçmek için, HeroListin yanına HeroDetaili ekleyin. Geçici olarak heroes dizisinin 0. indeksi olan bir hero özelliği olabilir (Green 1).

// src/heroes/Heroes.tsx
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import HeroList from "./HeroList";
import heroes from "./heroes.json";
import { useState } from "react";
import HeroDetail from "./HeroDetail";

export default function Heroes() {
  const [showModal, setShowModal] = useState<boolean>(false);
  const addNewHero = () => console.log("handleAdd");
  const handleRefresh = () => console.log("handleRefresh");
  const handleCloseModal = () => {
    setShowModal(false);
  };
  const handleDeleteHero = () => {
    setShowModal(true);
  };
  const handleDeleteFromModal = () => {
    setShowModal(false);
    console.log("handleDeleteFromModal");
  };

  return (
    <div data-cy="heroes">
      <ListHeader
        title="Heroes"
        handleAdd={addNewHero}
        handleRefresh={handleRefresh}
      />
      <div>
        <div>
          <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
          <HeroDetail hero={heroes[0]} />
        </div>
      </div>

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

Yolu değiştirdiğinde görüntülenen bileşeni değiştirmek istiyoruz. React'te bunu yönlendiren şey, önce yol, ardından alt bileşendir, react-router bölümünde gördüğümüz gibi. Bir bileşen testinde başlangıçta url yoktur, ancak bir bağlantıya tıkladığında yol değişir. Yolu kontrol etmek için yenileme ve + düğmeleri ile testi kullanabiliriz. Bu testte yolu kontrol etmek için cy.location kullanacağız. İşte Gleb Bahmutov'un Cypress ipuçlarından bir alıntı:

cy.visit("https://example.cypress.io/commands/location?search=value#top");
// yields a specific part of the location
cy.location("protocol").should("equal", "https:");
cy.location("hostname").should("equal", "example.cypress.io");
cy.location("pathname").should("equal", "/commands/location");
cy.location("search").should("equal", "?search=value");
cy.location("hash").should("equal", "#top");

Kısalık adına, test kodunu .only bölümüne odaklı tutacağız. Yenileme düğmesine tıklayarak yolun /heroes olmasını kontrol eden bir test yazıyoruz (Red 2).

// src/heroes/Heroes.cy.tsx
it.only("should display hero list on render", () => {
  cy.mount(
    <BrowserRouter>
      <Heroes />
    </BrowserRouter>
  );

  cy.getByCy("hero-list");
  cy.getByCy("hero-detail");
  cy.getByCy("refresh-button").click();
  cy.location("pathname").should("eq", "/heroes");
});

Test başarısız oldu, ancak konsolda handleRefresh günlüğünü görüyoruz. Günlüğün yerine, url'yi değiştiren bir şey kullanabiliriz. React-router'ın useNavigate işlevi, programlamalı olarak herhangi bir url'ye yönlendirmemize olanak tanır (Green 2).

// src/heroes/Heroes.tsx
import { useNavigate } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import HeroList from "./HeroList";
import heroes from "./heroes.json";
import { useState } from "react";
import HeroDetail from "./HeroDetail";

export default function Heroes() {
  const [showModal, setShowModal] = useState<boolean>(false);
  const addNewHero = () => console.log("handleAdd");
  const navigate = useNavigate();
  const handleRefresh = () => navigate("/heroes");

  const handleCloseModal = () => {
    setShowModal(false);
  };
  const handleDeleteHero = () => {
    setShowModal(true);
  };
  const handleDeleteFromModal = () => {
    setShowModal(false);
    console.log("handleDeleteFromModal");
  };

  return (
    <div data-cy="heroes">
      <ListHeader
        title="Heroes"
        handleAdd={addNewHero}
        handleRefresh={handleRefresh}
      />
      <div>
        <div>
          <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
          <HeroDetail hero={heroes[0]} />
        </div>
      </div>

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

Şimdi ekle düğmesine tıklayarak url'yi kontrol etmek için başka bir test deneyebiliriz. Yolun add-hero olmasını istiyoruz (Red 3).

// src/heroes/Heroes.cy.tsx
it.only("should display hero list on render", () => {
  cy.mount(
    <BrowserRouter>
      <Heroes />
    </BrowserRouter>
  );

  cy.getByCy("hero-list");
  cy.getByCy("hero-detail");

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

  cy.getByCy("add-button").click();
  cy.location("pathname").should("eq", "/heroes/add-hero");
});

Önceki döngüye benzer şekilde, handleAdd'ın console.logged olduğunu görüyoruz. Bir kez daha useNavigate kullanabiliriz (Yeşil 3).

// src/heroes/Heroes.tsx
import { useNavigate } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import HeroList from "./HeroList";
import heroes from "./heroes.json";
import { useState } from "react";
import HeroDetail from "./HeroDetail";

export default function Heroes() {
  const [showModal, setShowModal] = useState<boolean>(false);
  const navigate = useNavigate();
  const addNewHero = () => navigate("/heroes/add-hero");
  const handleRefresh = () => navigate("/heroes");

  const handleCloseModal = () => {
    setShowModal(false);
  };
  const handleDeleteHero = () => {
    setShowModal(true);
  };
  const handleDeleteFromModal = () => {
    setShowModal(false);
    console.log("handleDeleteFromModal");
  };

  return (
    <div data-cy="heroes">
      <ListHeader
        title="Heroes"
        handleAdd={addNewHero}
        handleRefresh={handleRefresh}
      />
      <div>
        <div>
          <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
          <HeroDetail hero={heroes[0]} />
        </div>
      </div>

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

Yol adlarımız iyi görünüyor, ancak ihtiyacımız olan şey, rotaya bağlı olarak farklı bileşenlerin görüntülenmesi.

react-router soyut rotalar

  1. İlk işlemde HeroList'i görmek istiyoruz.

  2. ListHeader'daki ekle düğmesine tıkladığında, HeroDetail'i görmek istiyoruz.

  3. ListHeader'daki yenile düğmesine tıkladığında tekrar HeroList'i görmek istiyoruz, böyle devam eder.

Burada biraz React Router v6 bilgisine ihtiyacımız var. Üst düzey uygulama bileşenindeki react-router kurulumunu hatırlayın. Burada /heroes rotası ile ilgileniyoruz. Yol adı sadece /heroes olduğunda HeroesList'i görüntülemek istiyoruz, /heroes/add-hero olduğunda HeroDetail'i görüntülemek istiyoruz. Bu, /heroes için soyut bir rota gerektireceği anlamına gelir.

// src/App.tsx
import About from "About";
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import NotFound from "components/NotFound";
import Heroes from "heroes/Heroes";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
        <main className="column">
          <Routes>
            <Route path="/" element={<Navigate replace to="/heroes" />} />
            {/* WE ARE CONCERNED ABOUT HEROES ROUTE */}
            <Route path="/heroes" element={<Heroes />} />
            <Route path="/about" element={<About />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
}

export default App;

react-router v6'da, bir rotanın soyut ağacında başka bir <Routes> olduğunda sonunda *'a ihtiyacımız var. Bu durumda, soyut <Routes> kalan yol adı kısmını eşleştirir. App.tsx dosyasındaki path="/heroes" özelliğini path="heroes/*" olarak değiştirmemiz gerekiyor. Bu, ekleyeceğimiz soyut Routes bileşeninin rota kontrolünü devralmasına izin verecektir.

// src/App.tsx
import About from "About";
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import NotFound from "components/NotFound";
import Heroes from "heroes/Heroes";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
        <main className="column">
          <Routes>
            <Route path="/" element={<Navigate replace to="/heroes" />} />
            <Route path="/heroes/*" element={<Heroes />} />
            <Route path="/about" element={<About />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
}

export default App;

Heroes bileşeninde ihtiyacımız olan şey şu:

<HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
<HeroDetail hero={heroes[0]} />

Bunu şuna dönüştürmek:

<Routes>
  <Route
    path=""
    element={<HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />}
  />

  <Route path="/add-hero" element={<HeroDetail />} />
</Routes>

Rota /heroes ise, HeroList'i görüntüleriz.

Rota /heroes/add-hero ise, HeroDetail'i görüntüleriz.

// src/heroes/Heroes.tsx
import { Route, Routes, useNavigate } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import HeroList from "./HeroList";
import heroes from "./heroes.json";
import { useState } from "react";
import HeroDetail from "./HeroDetail";

export default function Heroes() {
  const [showModal, setShowModal] = useState<boolean>(false);
  const navigate = useNavigate();
  const addNewHero = () => navigate("/heroes/add-hero");
  const handleRefresh = () => navigate("/heroes");

  const handleCloseModal = () => {
    setShowModal(false);
  };
  const handleDeleteHero = () => {
    setShowModal(true);
  };
  const handleDeleteFromModal = () => {
    setShowModal(false);
    console.log("handleDeleteFromModal");
  };

  return (
    <div data-cy="heroes">
      <ListHeader
        title="Heroes"
        handleAdd={addNewHero}
        handleRefresh={handleRefresh}
      />
      <div>
        <div>
          <Routes>
            <Route
              path=""
              element={
                <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
              }
            />

            <Route path="/add-hero" element={<HeroDetail />} />
          </Routes>
        </div>
      </div>

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

Bu yapıyla 2 başarısızlık var; test hiçbir şey görüntülemediği için başarısız oluyor, TS HeroDetail'in tanımlı bir hero ile hero özelliğine sahip olmasını istediği için hata veriyor (Kırmızı 4).

HeroDetail'i iki koşulda kullanmak için ayarlamıştık; heroId alanını heroId varsa veya yoksa görüntülemek için kullanabiliriz. Bu nedenle, yeni bir kahraman eklemek için bileşeni kullanabilmeliyiz. Şimdilik özelliği isteğe bağlı yapabiliriz ve boş id, name ve description özelliklerine sahip varsayılan bir kahraman nesnesi oluşturabiliriz. Şimdilik HeroDetail şu şekilde:

// src/heroes/HeroDetail.tsx
import InputDetail from "components/InputDetail";
import { useState, ChangeEvent } from "react";
import ButtonFooter from "components/ButtonFooter";
import { FaUndo, FaRegSave } from "react-icons/fa";

export type Hero = {
  id: string;
  name: string;
  description: string;
};
type HeroDetailProps = {
  hero?: Hero;
};

export default function HeroDetail({
  hero: initHero = {
    id: "",
    name: "",
    description: "",
  },
}: HeroDetailProps) {
  const [hero, setHero] = useState<Hero>({ ...initHero });

  const handleCancel = () => console.log("handleCancel");
  const updateHero = () => console.log("updateHero");
  const createHero = () => console.log("createHero");
  const handleSave = () => {
    console.log("handleSave");
    return hero.name ? updateHero() : createHero();
  };

  const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log("handleNameChange");
    setHero({ ...hero, name: e.target.value });
  };
  const handleDescriptionChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log("handleDescriptionChange");
    setHero({ ...hero, description: e.target.value });
  };

  return (
    <div data-cy="hero-detail" className="card edit-detail">
      <header className="card-header">
        <p className="card-header-title">{hero.name}</p>
        &nbsp;
      </header>
      <div className="card-content">
        <div className="content">
          {hero.id && (
            <InputDetail
              name={"id"}
              value={hero.id}
              readOnly={true}
            ></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
            onChange={handleNameChange}
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.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>
  );
}

HeroDetail.cy.tsx'in o değişiklikten sonra geçiyor olması harika. Tek endişemiz, Heroes testimizi bozmuş olmamız.

// src/heroes/Heroes.cy.tsx
it.only("should display hero list on render", () => {
  cy.mount(
    <BrowserRouter>
      <Heroes />
    </BrowserRouter>
  );

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

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

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

react-router bölümünden hatırlayacağımız gibi, bir bileşen testi yollar hakkında fikir sahibi değildir ve teste tıklayarak gezinmediğimiz sürece, yol belirsizdir. Bu, geçersiz bir kahramanlar rotası için de bir test gerektirebilir; örneğin heroes/foo42. Böyle bir durum söz konusu olduğunda, var olmayan bir heroId arıyoruz, HeroListi görmek istiyoruz. Yolun * olduğu HeroListi oluşturan yeni bir Route öğesi eklememiz gerekiyor.

// src/heroes/Heroes.tsx
import { useNavigate, Routes, Route, Navigate } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import HeroList from "./HeroList";
import heroes from "./heroes.json";
import { useState } from "react";
import HeroDetail from "./HeroDetail";

export default function Heroes() {
  const [showModal, setShowModal] = useState<boolean>(false);
  const navigate = useNavigate();
  const addNewHero = () => navigate("/heroes/add-hero");
  const handleRefresh = () => navigate("/heroes");

  const handleCloseModal = () => {
    setShowModal(false);
  };
  const handleDeleteHero = () => {
    setShowModal(true);
  };
  const handleDeleteFromModal = () => {
    setShowModal(false);
    console.log("handleDeleteFromModal");
  };

  return (
    <div data-cy="heroes">
      <ListHeader
        title="Heroes"
        handleAdd={addNewHero}
        handleRefresh={handleRefresh}
      />
      <div>
        <div>
          <Routes>
            <Route
              path=""
              element={
                <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
              }
            />
            <Route path="/add-hero" element={<HeroDetail />} />
            <Route
              path="*"
              element={
                <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
              }
            />
          </Routes>
        </div>
      </div>

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

Bileşenin testte bağlantısı belirsiz olduğundan, url doğrulamayı da HeroListin oluşturulduğunu kontrol etmeye değiştirmeliyiz (Green 4).

// src/heroes/Heroes.cy.tsx
it.only("should display the hero list on render", () => {
  cy.mount(
    <BrowserRouter>
      <Heroes />
    </BrowserRouter>
  );

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

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

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

Bu değişiklik, testin çalışmasını sağlar, ancak süite ilk iki it bloğu arasında uyumlu bir anlam kazandırmaz. handleAdd ve handleRefresh üzerindeki console.log'ları kontrol etmek için yapılan ilk test artık geçerli değil ve artık gerekmeyecek çünkü rotayı useNavigate ile değiştiriyoruz. useNavigate üzerinde casusluk yapabiliriz, ancak bu uygulama detayı ve zaten url'nin değiştiğini kontrol ediyoruz; daha yüksek seviyede, ekstra maliyet olmadan şeyleri daha iyi bir şekilde test ediyoruz. İşte testin yeniden düzenlenmesi (Düzenleme 4):

// src/heroes/Heroes.cy.tsx
import Heroes from "./Heroes";
import { BrowserRouter } from "react-router-dom";
import "../styles.scss";

describe("Heroes", () => {
  it("should display the hero list on render, and go through hero add & refresh flow", () => {
    cy.mount(
      <BrowserRouter>
        <Heroes />
      </BrowserRouter>
    );

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

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

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

  const invokeHeroDelete = () => {
    cy.getByCy("delete-button").first().click();
    cy.getByCy("modal-yes-no").should("be.visible");
  };
  it("should go through the modal flow", () => {
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.mount(
      <BrowserRouter>
        <Heroes />
      </BrowserRouter>
    );

    cy.getByCy("modal-yes-no").should("not.exist");

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

    cy.log("delete flow");
    invokeHeroDelete();
    cy.getByCy("button-yes").click();
    cy.getByCy("modal-yes-no").should("not.exist");
    cy.get("@log").should("have.been.calledWith", "handleDeleteFromModal");
  });
});

react-router bölümünde, yönlendirmeyi e2e testleri ile test etmenin en iyi yoluna karar verdik. Bileşenlerde yolları test ediyoruz, ancak yol değiştiğinde doğru alt bileşenin oluşturulup oluşturulmadığını test edemiyoruz. Benzer bir akışı kapsayan e2e testi başlatabiliriz, bu da gelecekte CRUD kahraman akışını kapsayan daha büyük bir test olarak hizmet edecektir. Daha düşük seviyede test edemeyeceğimiz veya güvenle test edemeyeceğimiz bir işlevsellik olduğunda, test piramidinde yukarı çıkarız, bu durumda bileşen testinden e2e testine. yarn cy:open-e2e ile e2e koşucusunu başlatın. cypress/e2e/create-hero.cy.ts adlı yeni bir e2e test oluşturun.

// cypress/e2e/create-hero.cy.ts

describe("Create hero", () => {
  beforeEach(() => cy.visit("/"));
  it("should go through the refresh flow", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("add-button").click();
    cy.location("pathname").should("eq", "/heroes/add-hero");
    cy.getByCy("hero-detail").should("be.visible");
    cy.getByCy("input-detail-id").should("not.exist");

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

Bu test, HeroDetail bileşenin ekleme sırasında nasıl görüntülendiğini ve yeni bir kahraman olduğundan id alanı olmadan nasıl görüntüleneceğini kontrol etmemize olanak tanır. Yenileme kahramanı oluşturma akışı bize yeni bir fikir verir; arka uç çalışıyor olsun veya olmasın, kahraman düzenleme veya ekleme için iptal akışı da çalışmalıdır. Belirli bir dönüm noktasına ulaştıktan sonra, uçtan uca testler veya uygulamanın basitçe reklam amaçlı kullanımı genellikle bize yeni özellikler için fikirler sunar. Sonuçta bu, bilimsel yöntemdir, şimdi daha fazlasını biliyoruz ve daha fazlasını denemek için çalışabiliriz ve bu, TDD'nin arkasındaki orijinal düşünceyi ve çevikliği esas alır.

Düzenleme kahramanı iptal akışı için başarısız bir e2e testi ekleyelim (Kırmızı 5). cypress/e2e/edit-hero.cy.ts adında bir dosya oluşturun. Ekleme akışına benzer şekilde başlar, ancak Düzenle düğmesine tıklar ve ilgili bir rota içinde olmayı bekler.

// cypress/e2e/edit-hero.cy.ts
describe("Edit hero", () => {
  beforeEach(() => cy.visit("/"));
  it("should go through the cancel flow", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("edit-button").first().click();
    cy.location("pathname").should("eq", "/heroes/edit-hero/HeroAslaug");
  });
});

Uygulamamızdaki tıklama işleyicileriyle ne yapacağımızdan emin olmadığımızda, onları console.log ile başlattık. E2e testinin konsolunda handleSelectHero'yu görebiliriz. Bu işlev HeroList bileşeninde bulunmaktadır. Sadece onu, ana Heroes bileşeninde yaptığımız gibi useNavigate kullanacak şekilde geliştirmemiz gerekiyor (Yeşil 5).

// src/heroes/HeroList.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 { Hero } from "models/Hero";
type HeroListProps = {
  heroes: Hero[];
  handleDeleteHero: () => void; // TODO: consider better type
};

export default function HeroList({ heroes, handleDeleteHero }: HeroListProps) {
  const navigate = useNavigate();
  const handleSelectHero = () => navigate("/heroes/edit-hero/HeroAslaug");

  return (
    <ul data-cy="hero-list" className="list">
      {heroes.map((hero, index) => (
        <li data-cy={`hero-list-item-${index}`} key={hero.id}>
          <div className="card">
            <CardContent name={hero.name} description={hero.description} />
            <footer className="card-footer">
              <ButtonFooter
                label="Delete"
                IconClass={FaRegSave}
                onClick={handleDeleteHero}
              />
              <ButtonFooter
                label="Edit"
                IconClass={FaEdit}
                onClick={handleSelectHero}
              />
            </footer>
          </div>
        </li>
      ))}
    </ul>
  );
}

İlk kahramana gidebiliriz, ancak başka bir kahramana gidip doğru URL'de bitebilir miyiz? Bunu test etmek için bir test yazalım (Kırmızı 6).

// cypress/e2e/edit-hero.cy.ts
describe("Edit hero", () => {
  beforeEach(() => cy.visit("/"));
  it("should go through the cancel flow", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("edit-button").first().click();
    cy.location("pathname").should("eq", "/heroes/edit-hero/HeroAslaug");
  });
  it("should go through the cancel flow for another hero", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("edit-button").eq(1).click();
    cy.location("pathname").should("eq", "/heroes/edit-hero/HeroBjorn");
  });
});

Test başarısız oluyor çünkü react-routerın rota parametresini bilmeye ihtiyacı var. heroId navigasyonundan daha iyi bir şey yapabilmemiz gerekiyor. Kahramanı düzenlerken, bu heroId'yi bileşene iletilen heroes özelliğinden alabilmeliyiz. handleSelectHero, id'yi bir argüman olarak almalı ve ona gitmelidir.

const handleSelectHero = (heroId: string) =>
  navigate(`/heroes/edit-hero/${heroId}`);

Bu değişiklik, şimdi handleSelectHero'nun bir argüman beklemesi nedeniyle ButtonFooer'da bir tür hatasına neden oluyor. Bileşeni şu şekilde güncelleyebiliriz (Yeşil 6):

// src/heroes/HeroList.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 { Hero } from "models/Hero";
type HeroListProps = {
  heroes: Hero[];
  handleDeleteHero: () => void; // TODO: consider better type
};

export default function HeroList({ heroes, handleDeleteHero }: HeroListProps) {
  const navigate = useNavigate();
  const handleSelectHero = (heroId: string) =>
    navigate(`/heroes/edit-hero/${heroId}`);

  return (
    <ul data-cy="hero-list" className="list">
      {heroes.map((hero, index) => (
        <li data-cy={`hero-list-item-${index}`} key={hero.id}>
          <div className="card">
            <CardContent name={hero.name} description={hero.description} />
            <footer className="card-footer">
              <ButtonFooter
                label="Delete"
                IconClass={FaRegSave}
                onClick={handleDeleteHero}
              />
              <ButtonFooter
                label="Edit"
                IconClass={FaEdit}
                onClick={() => handleSelectHero(hero.id)}
              />
            </footer>
          </div>
        </li>
      ))}
    </ul>
  );
}

Testi geliştirelim ve bir kahramanı düzenlerken sadece doğru URL yoluna sahip olmakla kalmayıp, aynı zamanda HeroDetail'i gösterdiğimizi kontrol edelim (Kırmızı 7).

// cypress/e2e/edit-hero.cy.ts
describe("Edit hero", () => {
  beforeEach(() => cy.visit("/"));
  it("should go through the cancel flow", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("edit-button").first().click();
    cy.location("pathname").should("eq", "/heroes/edit-hero/HeroAslaug");
    cy.location("pathname").should("include", "/heroes/edit-hero/");
    cy.getByCy("hero-detail").should("be.visible");
  });
  it("should go through the cancel flow for another hero", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("edit-button").eq(1).click();
    cy.location("pathname").should("eq", "/heroes/edit-hero/HeroBjorn");
    cy.location("pathname").should("include", "/heroes/edit-hero/");
    cy.getByCy("hero-detail").should("be.visible");
  });
});

Yol özellikleri ve useParams

Yoldaki heroId'yi çıkarmanın ve bileşenin bunu bilmesini sağlamanın bir yoluna ihtiyacımız var. react-routerda yol özelliklerinden ve useParam kancasından yararlanabiliriz. İşte yol özelliklerinin nasıl çalıştığını gösteren basit bir örnek. Verimizin milkshake olduğunu ve veri modelinin şu şekilde göründüğünü varsayalım:

{
  "flavor": "vanilla",
  "size": "medium"
}

Eğer rota ayarlarını böyle yaparsak:

<Route path="/milkshake/:flavor/:size" element={<Milkshake />} />

URL yolu /milkshake/vanilla/medium olacaktır.

Bu yapılandırmayı yeniden oluşturmak için, edit-hero yolunun id adında bir yol özelliğine ihtiyacı vardır ve URL'den bu yol özelliğini çıkarmanın bir yoluna ihtiyacımız vardır. React-router'ın useParamı, URL parametrelerine karşılık gelen özelliklere sahip bir nesne döndürür..

const { flavor, size } = useParams();

Bu bilgiyi uygulamamıza aktararak, veri şu şekildedir:

{
  "id": "HeroAslaug",
  "name": "Aslaug",
  "description": "warrior queen"
},

Yollara kıyasla şu şekildedir:

<Route path="/milkshake/:flavor/:size" element={<Milkshake />} />
<Route path="/edit-hero/:id" element={<HeroDetail />} />

useParams() böyle olabilir:

const { id } = useParams();

Heroes bileşenindeki HeroDetail rotasını, edit-hero yoluna bir :id rotası parametresi ekleyerek değiştirin (Yeşil 7).

// src/heroes/Heroes.tsx
import { useNavigate, Routes, Route } from "react-router-dom";
import ListHeader from "components/ListHeader";
import ModalYesNo from "components/ModalYesNo";
import HeroList from "./HeroList";
import heroes from "./heroes.json";
import { useState } from "react";
import HeroDetail from "./HeroDetail";

export default function Heroes() {
  const [showModal, setShowModal] = useState<boolean>(false);
  const navigate = useNavigate();
  const addNewHero = () => navigate("/heroes/add-hero");
  const handleRefresh = () => navigate("/heroes");

  const handleCloseModal = () => {
    setShowModal(false);
  };
  const handleDeleteHero = () => {
    setShowModal(true);
  };
  const handleDeleteFromModal = () => {
    setShowModal(false);
    console.log("handleDeleteFromModal");
  };

  return (
    <div data-cy="heroes">
      <ListHeader
        title="Heroes"
        handleAdd={addNewHero}
        handleRefresh={handleRefresh}
      />
      <div>
        <div>
          <Routes>
            <Route
              path=""
              element={
                <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
              }
            />
            <Route path="/add-hero" element={<HeroDetail />} />
            <Route path="/edit-hero/:id" element={<HeroDetail />} />
            <Route
              path="*"
              element={
                <HeroList heroes={heroes} handleDeleteHero={handleDeleteHero} />
              }
            />
          </Routes>
        </div>
      </div>

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

Test geçiyor, heroId ile doğru URL'ye sahibiz, HeroDetails görüntüleniyor, ancak heroId alanı görüntülenmiyor.

Bir kahramanı düzenlerken heroId alanının görünür olduğundan emin olmak için bir test daha yazıyoruz (Kırmızı 8).

// cypress/e2e/edit-hero.cy.ts
describe("Edit hero", () => {
  beforeEach(() => cy.visit("/"));
  it("should go through the cancel flow", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("edit-button").first().click();
    cy.location("pathname").should("eq", "/heroes/edit-hero/HeroAslaug");
    cy.location("pathname").should("include", "/heroes/edit-hero/");
    cy.getByCy("hero-detail").should("be.visible");
    cy.getByCy("input-detail-id").should("be.visible");
  });
  it("should go through the cancel flow for another hero", () => {
    cy.location("pathname").should("eq", "/heroes");

    cy.getByCy("edit-button").eq(1).click();
    cy.location("pathname").should("eq", "/heroes/edit-hero/HeroBjorn");
    cy.location("pathname").should("include", "/heroes/edit-hero/");
    cy.getByCy("hero-detail").should("be.visible");
    cy.getByCy("input-detail-id").should("be.visible");
  });
});

URL'den id değerini almak için, useParams() ile const { id } = useParams() kullanarak rotayı bileşene bağlayın. Kahraman verilerine güvenmek yerine, URL'den aldığımız yol özniteliğine güvenmek istiyoruz ve useParams bunun için kullanılacak kancadır. Ayrıca, doğrudan bir URL'ye yönlendirebilme yan faydasına da sahibiz (Yeşil 8).

// src/heroes/HeroDetail.tsx
import { useState, ChangeEvent } from "react";
import { useParams } from "react-router-dom";
import { FaUndo, FaRegSave } from "react-icons/fa";
import InputDetail from "components/InputDetail";
import ButtonFooter from "components/ButtonFooter";

export type Hero = {
  id: string;
  name: string;
  description: string;
};
type HeroDetailProps = {
  hero?: Hero;
};

export default function HeroDetail({
  hero: initHero = {
    id: "",
    name: "",
    description: "",
  },
}: HeroDetailProps) {
  const { id } = useParams();
  const [hero, setHero] = useState<Hero>({ ...initHero });

  const handleCancel = () => console.log("handleCancel");
  const updateHero = () => console.log("updateHero");