ch10-HeroDetail

Daha basit, çocuk bileşenlerle işimiz bitti. Bundan sonra çocuk bileşenleri kullanan ve uygulama durumunu göz önünde bulunduran daha üst düzey bileşenlere odaklanacağız.

Angular sürümünde uygulamanın, şimdiye kadar gördüklerimizden biraz daha karmaşık bir bileşeni bulunuyor. Bir bileşen karmaşık göründüğünde, en üst düzeyden başlayarak katman katman aşağı inerek anlamlandırmak daha kolaydır.

  • başlık

    • kahramanın adı ile p.

  • div/div

    • InputDetail bileşenini kullanan 3 alan. İlk alan readonly.

  • altbilgi

    • ButtonFooter bileşenini kullanan 2 altbilgi; İptal ve Kaydet çeşidi.

Test odaklı tasarım, mühendislik ve bilimsel yöntem, hepsi daha küçük parçalara ayırarak sorunu çözmeye, kısa geri bildirim döngüleriyle testler aracılığıyla ilerlememizi doğrulamaya ve hızla tekrarlamaya dayanır. Cypress bileşen testlerini iyi bir uyum haline getiren şey, geri bildirim döngülerinin kalitesi ve hızıdır.

feat/HeroDetail adında bir dal oluşturun. src/heroes/ klasörü altında HeroDetail.cy.tsx, HeroDetail.tsx adında 2 dosya oluşturun. Her zamanki gibi, bileşen oluşturma işlemini basitleştirerek başlayın; aşağıdakileri dosyalara kopyalayın ve yarn cy:open-ct komutuyla koşucuyu açtıktan sonra testi çalıştırın.

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should", () => {
    cy.mount(<HeroDetail />);
  });
});
// src/heroes/HeroDetail.tsx

export default function HeroDetail() {
  return <div>hello</div>;
}

Bileşenin üst düzey katmanlarına bakarak, bir taslakla başarısız bir test yazmaya başlayabiliriz (Kırmızı 1).

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should", () => {
    cy.mount(<HeroDetail />);

    cy.getByCy("hero-detail").contains("my hero");
    cy.contains("header", "my hero");
  });
});

Başlangıçta şimdilik sadece en temel gereksinime odaklanıyoruz; bu testi geçmek için sadece data-cy özniteliği ve sert kodlanmış bir değer içeren bir başlık etiketi gereklidir (Yeşil 1).

// src/heroes/HeroDetail.tsx

export default function HeroDetail() {
  return (
    <div data-cy="hero-detail">
      <header>
        <p>my hero</p>
      </header>
    </div>
  );
}

3 form alanı

InputDetail bileşenleri için sonraki etiketler, form alanlarını oluşturacak olan 3 alandır. Uzunluklarının 3 olması gerektiğini kontrol edelim (Kırmızı 2).

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should", () => {
    cy.mount(<HeroDetail />);

    cy.getByCy("hero-detail").contains("my hero");
    cy.contains("header", "my hero");
    cy.getByCyLike("input-detail").should("have.length", 3);
  });
});

InputDetail bileşenlerini iki katmanlı divlerin altına ekleyin ve geçen bir test alın (Yeşil 2).

// src/heroes/HeroDetail.tsx
import InputDetail from "../components/InputDetail";

export default function HeroDetail() {
  return (
    <div data-cy="hero-detail">
      <header>
        <p>my hero</p>
      </header>
      <div>
        <div>
          <InputDetail></InputDetail>
          <InputDetail></InputDetail>
          <InputDetail></InputDetail>
        </div>
      </div>
    </div>
  );
}

TS bize yardımcı oluyor ve InputDetail bileşeninin bazı prop'larla gelmesi gerektiğini bildiriyor. Derleyiciyi kullanıp otomatik olarak düzeltecek olursak, zorunlu prop'ları ekler (Kırmızı 3, Yeşil 3).

// src/heroes/HeroDetail.tsx
import InputDetail from "../components/InputDetail";

export default function HeroDetail() {
  return (
    <div data-cy="hero-detail">
      <header>
        <p>my hero</p>
      </header>
      <div>
        <div>
          <InputDetail name={""} value={""}></InputDetail>
          <InputDetail name={""} value={""}></InputDetail>
          <InputDetail name={""} value={""}></InputDetail>
        </div>
      </div>
    </div>
  );
}

Şartnamelere göz atarak testimizi geliştirmeye başlayabiliriz. İlk InputDetail alanının readonly olacağını biliyoruz. Yazılabilir iki alanın yer tutucu metinleri olmalıdır. Testing Library örnekleri form alanlarında metin kontrol etmek için iki kullanışlı komut içerir; findByDisplayValue, findByPlaceholderText (Kırmızı 4).

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should", () => {
    cy.mount(<HeroDetail />);

    cy.getByCy("hero-detail").contains("my hero");
    cy.contains("header", "my hero");
    cy.getByCyLike("input-detail").should("have.length", 3);

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

Testi geçmek için gerekenleri yapmalıyız. İlk uygulama ekran görüntüsünden alan adlarını alabiliriz; alanların adları id, name, description olmalıdır. İlk alan id değeri şimdilik sabit kodlanabilir. İkinci ve üçüncü alanların boş değeri ve placeholderı vardır. Bu, yeşil bir test almak için minimaldir (Yeşil 4).

// src/heroes/HeroDetail.tsx
import InputDetail from "../components/InputDetail";

export default function HeroDetail() {
  return (
    <div data-cy="hero-detail">
      <header>
        <p>my hero</p>
      </header>
      <div>
        <div>
          <InputDetail name={"id"} value={"HeroAslaug"}></InputDetail>
          <InputDetail
            name={"name"}
            value=""
            placeholder={"e.g. Colleen"}
          ></InputDetail>
          <InputDetail
            name={"description"}
            value=""
            placeholder={"e.g. dance fight!"}
          ></InputDetail>
        </div>
      </div>
    </div>
  );
}

Düzenleme etmeye başlamadan önce, bu aşamada kahraman verilerinin şekli hakkında konuşmak yararlıdır. Ağdan id'yi (salt okunur) okuyacak ve name ve description'ı ağa yazacağız. Verilerimiz şu şekilde 3 string özelliğe sahip bir nesnedir:

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

Bu şekil için bir arayüz oluşturalım, çünkü her yerde kullanacağız. ./src/models/Hero.ts dosyası ve klasör oluşturun ve aşağıdaki kodu yapıştırın:

// /src/models/Hero.ts
export interface Hero {
  id: string;
  name: string;
  description: string;
}

Bileşenimizde yer tutucu metinlerin sabit kodlanması sorun oluşturmaz, ancak value özellikleri dikkat çekicidir. Bu, uygulamamızda durum ihtiyacına işaret ediyor. Şimdilik bunu bileşene sabit kodlayarak başlayabiliriz (Düzenleme 4).

// src/heroes/HeroDetail.tsx
import { Hero } from "../models/Hero";
import InputDetail from "../components/InputDetail";

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "",
    description: "",
  };
  return (
    <div data-cy="hero-detail">
      <header>
        <p>my hero</p>
      </header>
      <div>
        <div>
          <InputDetail
            name={"id"}
            value={hero.id}
            readOnly={true}
          ></InputDetail>
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
    </div>
  );
}

TypeScript yollarını kullanarak çok fazla ../ klasör başvurusuna ihtiyaç duymadan dosyaları bileşenler, modeller ve gelecekte kolayca içe aktarabiliriz.

{
  "compilerOptions": {
    "target": "esnext",
    "lib": ["esnext", "dom"],
    "types": ["cypress", "node", "@testing-library/cypress"],
    "baseUrl": "./"
  },
  "include": ["**/*.ts*", "../cypress.d.ts"],
  "extends": "../tsconfig.json"
}

Şimdi bileşenlerden, modellerden ve gelecekteki kancalardan dosyaları daha kolay bir şekilde içe aktarabiliriz.

// src/heroes/HeroDetail.tsx
import { Hero } from "models/Hero";
import InputDetail from "components/InputDetail";

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "",
    description: "",
  };
  return (
    <div data-cy="hero-detail">
      <header>
        <p>my hero</p>
      </header>
      <div>
        <div>
          <InputDetail
            name={"id"}
            value={hero.id}
            readOnly={true}
          ></InputDetail>
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
    </div>
  );
}

Hala "my-hero" kodlamasını test ve bileşene gömülü olarak yapıyoruz. Bu açıkça hero.name ve uygulama ekran görüntüsünde bile görüntülenmiyor. Ağ verisi olsun ya da olmasın bunu görüntülemek için bir mekanizmaya ihtiyacımız var. Veriler bileşene gömülü olduğu için şimdilik bunları testlerle kontrol edemeyeceğiz, bu yüzden şimdilik contains('my hero') ile metin kontrollerini devre dışı bırakabilir ve bileşen üzerinde çalışabiliriz.

Test ve bileşenimiz şu anda şu şekildedir:

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should", () => {
    cy.mount(<HeroDetail />);

    cy.getByCy("hero-detail");
    cy.getByCyLike("input-detail").should("have.length", 3);

    cy.findByDisplayValue("HeroAslaug").should("be.visible");
    cy.findByPlaceholderText("e.g. Colleen").should("be.visible");
    cy.findByPlaceholderText("e.g. dance fight!").should("be.visible");
  });
});
// src/heroes/HeroDetail.tsx
import { Hero } from "models/Hero";
import InputDetail from "components/InputDetail";

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "",
    description: "",
  };
  return (
    <div data-cy="hero-detail">
      <header>
        <p>{hero.name}</p>
      </header>
      <div>
        <div>
          <InputDetail
            name={"id"}
            value={hero.id}
            readOnly={true}
          ></InputDetail>
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
    </div>
  );
}

Eğer hero.name için bir dize girersek, bileşen test yürütücüsünde p'yi açıp kapatabiliriz. Aynı mantığı, id alanı için de uygulamamız gerekiyor çünkü eğer id için veri mevcut değilse, görüntülemenin mantıklı olmadığı ortada. Bunu koşullu oluşturma ile sağlayabiliriz.

// src/heroes/HeroDetail.tsx
import { Hero } from "models/Hero";
import InputDetail from "components/InputDetail";

export default function HeroDetail() {
  const hero: Hero = {
    id: "",
    name: "",
    description: "",
  };
  return (
    <div data-cy="hero-detail">
      <header>
        <p>{hero.name}</p>
      </header>
      <div>
        <div>
          {hero.id && (
            <InputDetail
              name={"id"}
              value={hero.id}
              readOnly={true}
            ></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
    </div>
  );
}

hero.id değerini değiştirin ve alan da değiştirilmelidir. Şimdilik 2 alan veya daha fazlasıyla tamam olacak şekilde testi ayarlamamız gerekiyor (Düzenleme 4).

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should", () => {
    cy.mount(<HeroDetail />);

    // cy.getByCy('hero-detail').contains('my hero')
    // cy.contains('header', 'my hero')
    // cy.getByCyLike('input-detail').should('have.length', 3)
    cy.getByCy("hero-detail");
    cy.getByCyLike("input-detail").should("have.length.gte", 2);

    // cy.findByDisplayValue('HeroAslaug').should('be.visible')
    cy.findByPlaceholderText("e.g. Colleen").should("be.visible");
    cy.findByPlaceholderText("e.g. dance fight!").should("be.visible");
  });
});

id yoksa, id alanı devre dışı kalacak ve tam tersi. İsim varsa, p gösterilecektir ve tam tersi. Bunların hepsi daha sonra ele alacağımız test durumlarıdır. Testlerin, ihtiyaç duyduğumuz tasarımı doğrulamak için kullanamayacağımızı belirtmek önemlidir, ancak bileşen testinin mini bir kullanıcı arayüzü uygulaması olması, şimdilik bize yardımcı oluyor.

Tam UI düzenine sahip olana kadar durumla ilgili kararları erteleyeceğiz.

Aşağılık bölümü, Cancel ve Save düğmeleri için 2 ButtonFooter bileşenini saran bir footer etiketinden oluşur. Bunun için başarısız bir test yazalım. İlgili seçici ile ButtonFooter bileşenine bakıyoruz; save-button, cancel-button (Kırmızı 5)

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should verify the layout of the component", () => {
    cy.mount(<HeroDetail />);

    // cy.getByCy('hero-detail').contains('my hero')
    // cy.contains('header', 'my hero')
    // cy.getByCyLike('input-detail').should('have.length', 3)
    cy.getByCy("hero-detail");
    cy.getByCyLike("input-detail").should("have.length.gte", 2);

    // cy.findByDisplayValue('HeroAslaug').should('be.visible')
    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");
  });
});

ButtonFooter alt bileşenlerini eklediğimizde, TS hataları ve başarısız bir test (Kırmızı 5) alırız.

// src/heroes/HeroDetail.tsx
import InputDetail from "components/InputDetail";
import ButtonFooter from "components/ButtonFooter";

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "Aslaug",
    description: "",
  };
  return (
    <div data-cy="hero-detail">
      <header>
        <p>{hero.name}</p>
      </header>
      <div>
        <div>
          {hero.id && (
            <InputDetail
              name={"id"}
              value={hero.id}
              readOnly={true}
            ></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
      <footer>
        <ButtonFooter />
        <ButtonFooter />
      </footer>
    </div>
  );
}

ButtonFooter prop'ları label, IconClass ve onClick'tır. TS ve onun için yazdığımız bileşen testi /components/ButtonFooter.cy.tsx, belgeleme görevi görür. Bileşen testine bakarak eksik prop'ları ekleyelim. Şimdilik, tıklama işleyicileri boş fonksiyonlar olabilir. Simgeleri react-icons adresinden alabiliriz. label herhangi bir dize olabilir (Yeşil 5).

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

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "Aslaug",
    description: "",
  };
  return (
    <div data-cy="hero-detail">
      <header>
        <p>{hero.name}</p>
      </header>
      <div>
        <div>
          {hero.id && (
            <InputDetail
              name={"id"}
              value={hero.id}
              readOnly={true}
            ></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
      <footer>
        <ButtonFooter label="Cancel" IconClass={FaUndo} onClick={() => {}} />
        <ButtonFooter label="Save" IconClass={FaRegSave} onClick={() => {}} />
      </footer>
    </div>
  );
}

Bu formu kaydederken veya iptal ederken durumu değiştireceğiz, bu nedenle bir olay meydana gelmelidir. Şimdilik console.log'u izleyen başarısız testler yazabiliriz (Kırmızı 6).

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";

describe("HeroDetail", () => {
  it("should verify the layout of the component", () => {
    cy.mount(<HeroDetail />);

    // cy.getByCy('hero-detail').contains('my hero')
    // cy.contains('header', 'my hero')
    // cy.getByCyLike('input-detail').should('have.length', 3)
    cy.getByCy("hero-detail");
    cy.getByCyLike("input-detail").should("have.length.gte", 2);

    // cy.findByDisplayValue('HeroAslaug').should('be.visible')
    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");
  });
  it("should handle Save", () => {
    cy.mount(<HeroDetail />);
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.getByCy("save-button").click();
    cy.get("@log").should("have.been.calledWith", "handleSave");
  });

  it("should handle Cancel", () => {
    cy.mount(<HeroDetail />);
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.getByCy("cancel-button").click();

    cy.get("@log").should("have.been.calledWith", "handleCancel");
  });
});

Testleri geçirir hale getirmek için işleyiciler için console.log ekleyebiliriz (Yeşil 6).

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

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "Aslaug",
    description: "",
  };
  return (
    <div data-cy="hero-detail">
      <header>
        <p>{hero.name}</p>
      </header>
      <div>
        <div>
          {hero.id && (
            <InputDetail
              name={"id"}
              value={hero.id}
              readOnly={true}
            ></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
      <footer>
        <ButtonFooter
          label="Cancel"
          IconClass={FaUndo}
          onClick={() => {
            console.log("handleCancel");
          }}
        />
        <ButtonFooter
          label="Save"
          IconClass={FaRegSave}
          onClick={() => {
            console.log("handleSave");
          }}
        />
      </footer>
    </div>
  );
}

console.log'ları yardımcı fonksiyonlara dönüştürebiliriz (Düzenleme 6)

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

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "Aslaug",
    description: "",
  };

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

  return (
    <div data-cy="hero-detail">
      <header>
        <p>{hero.name}</p>
      </header>
      <div>
        <div>
          {hero.id && (
            <InputDetail
              name={"id"}
              value={hero.id}
              readOnly={true}
            ></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
      <footer>
        <ButtonFooter
          label="Cancel"
          IconClass={FaUndo}
          onClick={handleCancel}
        />
        <ButtonFooter label="Save" IconClass={FaRegSave} onClick={handleSave} />
      </footer>
    </div>
  );
}

Bir kahramanı kaydederken, kahramanı oluşturuyor veya güncelliyor olacağız. Eğer hero.name yoksa, onu oluşturmalıyız. Eğer bir hero.name varsa, kahramanı güncellemeliyiz. handleSave işlemini kullanarak güncelleme ve kaydetme fonksiyonları oluşturalım ve mantığını geliştirelim. Şimdilik, name özelliğini kullanarak test edebilir ve handleSave çağrıldığında konsolun updateHero ve createHero arasında geçiş yapmasını sağlayabiliriz.

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

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "Aslaug",
    description: "",
  };

  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();
  };

  return (
    <div data-cy="hero-detail">
      <header>
        <p>{hero.name}</p>
      </header>
      <div>
        <div>
          {hero.id && (
            <InputDetail
              name={"id"}
              value={hero.id}
              readOnly={true}
            ></InputDetail>
          )}
          <InputDetail
            name={"name"}
            value={hero.name}
            placeholder="e.g. Colleen"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
      <footer>
        <ButtonFooter
          label="Cancel"
          IconClass={FaUndo}
          onClick={handleCancel}
        />
        <ButtonFooter label="Save" IconClass={FaRegSave} onClick={handleSave} />
      </footer>
    </div>
  );
}

Durum konusuna geçmeden önce, stiller ekleyelim (Düzenleme 6).

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

export default function HeroDetail() {
  const hero: Hero = {
    id: "HeroAslaug",
    name: "Aslaug",
    description: "",
  };

  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();
  };

  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"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
      <footer className="card-footer">
        <ButtonFooter
          label="Cancel"
          IconClass={FaUndo}
          onClick={handleCancel}
        />
        <ButtonFooter label="Save" IconClass={FaRegSave} onClick={handleSave} />
      </footer>
    </div>
  );
}

Durum ve useState

Kent C. Dodds'tan alıntı yaparak, React'te UI durum yönetimini iki kategoriye indirgeyebiliriz:

  1. Kullanıcı Arayüzü durumu: modal açık, öğe vurgulanmış vb.

  2. Sunucu verileri

Herhangi bir durumu uygulamadan önce, bileşenin düzenini inceleyen testler yazabiliriz. Bileşendeki sabit kodlu hero nesnesi yerine, bir prop ile veri iletebiliriz. Bileşenlerimizi prop'lar veya onları saran şeyler üzerinden değiştiriyoruz ve şu anda prop daha kolay bir seçenek. Prop'un değeri sadece kahraman nesnemizdir (Kırmızı 7).

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";
import { Hero } from "models/Hero";

describe("HeroDetail", () => {
  it("should handle Save", () => {
    cy.mount(<HeroDetail />);
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.getByCy("save-button").click();
    cy.get("@log").should("have.been.calledWith", "handleSave");
  });

  it("should handle Cancel", () => {
    cy.mount(<HeroDetail />);
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.getByCy("cancel-button").click();

    cy.get("@log").should("have.been.calledWith", "handleCancel");
  });

  context("state: should verify the layout of the component", () => {
    it("id: false, name: false - should verify the minimal state of the component", () => {
      const hero: Hero = { id: "", name: "", description: "" };
      cy.mount(<HeroDetail hero={hero} />);

      cy.getByCy("hero-detail");
      cy.getByCyLike("input-detail").should("have.length", 2);

      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");
    });
  });
});

Daha önce, kaydetme ve iptal işlemlerini ele almak için yer tutucular oluşturduk, bunlar kategori 2'ye (sunucu verileri) girer. Ayrıca ad ve açıklama alanlarında durum da bulunmaktadır ve bunlar kategori 1 (kullanıcı arayüzü durumu) altına girer. Durumu en alakalı olduğu yerde yönetmeyi tercih etmek istiyoruz. React'te bunu yapmanın en temel yolu, useState hook'u ile yapılabilir. Bir bileşen içinde kullanılan değerin değiştiğine dair React'a uyarıda bulunmak istiyoruz ve sadece değişkeni doğrudan güncellemek işe yaramaz, bir güncelleyici işleve ihtiyacımız var. Bu yaklaşımda:

  1. Bileşenin ihtiyaç duyduğu durumu düşünün

  2. Durumu gösterin

  3. Olaylara yanıt olarak durumu güncelleyin

useState hook'u, değeri ve güncelleyici işlevi 2 elemanlı bir dizi içinde döndürür, adlar keyfidir. Değişken için başlangıç değeri istiyorsak, bunu useState'e argüman olarak iletiyoruz.

const [value, setValue] = useState(initialValue);

Bizim durumumuzda bu şöyle olabilir:

const [hero, setHero] = useState(someInitialHeroData);

someInitialHeroData değişken adı uzundur. HeroDetail bileşenini kullanan herkesin perspektifinden, bu sadece hero olarak görülür. Bileşen içindeki perspektiften de öyledir. Bunu çözmek için adı takma adla kullanabilir ve geçirilen kahramanın bir kopyasını oluşturarak nesne yapılandırmasını kullanabiliriz.

Testlerde belirttiğimiz gibi, bileşenimize herhangi bir veriyi iletmek için en basit yol, bir proptur. Bileşenimizi durum yönetimi için hazırlanmak üzere yeniden düzenleyebiliriz. Bu şekilde, bileşende sabit kodlu hero verisi olması gerekmez ve bunun bileşeni kullanan kişi tarafından belirlenmesine izin verebiliriz. Bir kez prop iletilince, useState hook'unu bileşen durumunu yönetmek için kullanabiliriz (Yeşil 7).

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

type HeroDetailProps = {
  hero: Hero;
};

export default function HeroDetail({ hero: initHero }: 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();
  };

  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"
          ></InputDetail>
          <InputDetail
            name={"description"}
            value={hero.description}
            placeholder="e.g. dance fight!"
          ></InputDetail>
        </div>
      </div>
      <footer className="card-footer">
        <ButtonFooter
          label="Cancel"
          IconClass={FaUndo}
          onClick={handleCancel}
        />
        <ButtonFooter label="Save" IconClass={FaRegSave} onClick={handleSave} />
      </footer>
    </div>
  );
}

ESLint, setHero'nun kullanılmadığı konusunda bizi uyarıyor. InputDetail'in yazılabilir alanlar için kullanılan bir onChange işleyicisi olduğunu fark edin ve burada setHero uygun şekilde yer alıyor. Şimdi ad değişikliğini ve açıklama değişikliğini ele almak için iki test daha yazalım. Ayrıca, TS, handleSave ve handleCancel testlerinde eksik proplar hakkında bize bir uyarı veriyor. Tüm bu handleSomething testleri için, bileşene ilettiğimiz prop olarak boş özelliklere sahip bir kahraman nesnesi kullanabiliriz (Kırmızı 8).

// src/heroes/HeroDetail.cy.tsx
import HeroDetail from "./HeroDetail";
import "../styles.scss";
import { Hero } from "models/Hero";
import React from "react";

describe("HeroDetail", () => {
  it("should handle Save", () => {
    const hero: Hero = { id: "", name: "", description: "" };
    cy.mount(<HeroDetail hero={hero} />);
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.getByCy("save-button").click();
    cy.get("@log").should("have.been.calledWith", "handleSave");
  });

  it("should handle Cancel", () => {
    const hero: Hero = { id: "", name: "", description: "" };
    cy.mount(<HeroDetail hero={hero} />);
    cy.window()
      .its("console")
      .then((console) => cy.spy(console, "log").as("log"));

    cy.getByCy("cancel-button").click();

    cy.get("@log").should("have.been.calledWith", "handleCancel");
  });

  context("handleNameChange, handleDescriptionChange", () => {
    it("should handle name change", () => {
      const hero: Hero = { id: "", name: "", description: "" };
      cy.mount(<HeroDetail hero={hero} />);
      cy.window()
        .its("console")
        .then((console) => cy.spy(console, "log").as("log"));

      cy.getByCy("input-detail-name").type("abc");
      cy.get("@log").should("have.been.calledWith", "handleNameChange");
      cy.get("@log").its("callCount").should("eq", 3);
    });

    it("should handle description change", () => {
      const hero: Hero = { id: "", name: "", description: "" };
      cy.mount(<HeroDetail hero={hero} />);
      cy.window()
        .its("console")
        .then((console) => cy.spy(console, "log").as("log"));

      cy.getByCy("input-detail-description").type("123");
      cy.get("@log").should("have.been.calledWith", "handleDescriptionChange");
      cy.get("@log").its("callCount").should("eq", 3);
    });
  });

  context("state: should verify the layout of the component", () => {
    it("id: false, name: false - should verify the minimal state of the component", () => {
      const hero: Hero = { id: "", name: "", description: "" };
      cy.mount(<HeroDetail hero={hero} />);

      cy.getByCy("hero-detail");
      cy.getByCyLike("input-detail").should("have.length", 2);

      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");
    });
  });
});

Bileşene eksik işleyicileri ekleyerek testin geçmesini sağlayabiliriz (Yeşil 8).