ViraStack
  • Academy
  • About
  • Support
HomeSourcesContribute
ViraStack

A DX-first toolkit for frontend developers: starters, agent rules, form hooks, and a guide.

Products

  • ViraStack Start
  • ViraStack AI
  • ViraStack Mask
  • ViraStack Password
  • ViraStack Guide

Explore

  • Academy
  • About
  • Brand
  • Support

© 2026 ViraStack.

Open source · MIT

Vitest

08 / 12Project

Ürün Listesi

Yükleme, arama ve sepete ekleme akışı.

Senaryo

Mağaza ana sayfası ürünleri bir API'den yükler. Liste şunları yapar:

  • Yüklenirken Ürünler yükleniyor... mesajı gösterir.

  • Yükleme başarısız olursa Ürünler yüklenemedi. mesajı gösterir.

  • Arama kutusuna yazılan metne göre ürünleri filtreler.

  • Eşleşme yoksa Ürün bulunamadı. mesajı gösterir.

  • Her ürün için Sepete ekle butonu sunar.

Component

loadProducts fonksiyonu dışarıdan verilir. Testte gerçek API yerine kendi fonksiyonunuzu verirsiniz.

import { useEffect, useState } from "react";
import { ProductCard } from "./ProductCard";
import type { Product } from "./types";

type ProductListProps = {
  loadProducts: () => Promise<Product[]>;
  onAddToCart: (product: Product) => void;
};

type Status = "loading" | "error" | "ready";

export function ProductList({ loadProducts, onAddToCart }: ProductListProps) {
  const [products, setProducts] = useState<Product[]>([]);
  const [status, setStatus] = useState<Status>("loading");
  const [query, setQuery] = useState("");

  useEffect(() => {
    let ignore = false;

    loadProducts()
      .then((loaded) => {
        if (ignore) return;
        setProducts(loaded);
        setStatus("ready");
      })
      .catch(() => {
        if (ignore) return;
        setStatus("error");
      });

    return () => {
      ignore = true;
    };
  }, [loadProducts]);

  if (status === "loading") {
    return <p role="status">Ürünler yükleniyor...</p>;
  }

  if (status === "error") {
    return <p role="alert">Ürünler yüklenemedi.</p>;
  }

  const normalizedQuery = query.trim().toLocaleLowerCase("tr-TR");
  const visibleProducts = products.filter((product) =>
    product.name.toLocaleLowerCase("tr-TR").includes(normalizedQuery),
  );

  return (
    <section aria-label="Ürünler">
      <label htmlFor="product-search">Ürün ara</label>
      <input
        id="product-search"
        type="search"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      {visibleProducts.length === 0 ? (
        <p>Ürün bulunamadı.</p>
      ) : (
        <ul>
          {visibleProducts.map((product) => (
            <li key={product.id}>
              <ProductCard product={product} onAddToCart={onAddToCart} />
            </li>
          ))}
        </ul>
      )}
    </section>
  );
}

Effect içindeki ignore bayrağı, loadProducts değiştiğinde ya da component kaldırıldığında eski cevabın state'i güncellemesini engeller.1 Arama toLocaleLowerCase("tr-TR") kullanır. Türkçe gibi dillerde büyük-küçük harf eşlemesi varsayılandan farklı olabilir.2

Testler

Önce ortak veriyi ve bir yardımcı fonksiyonu yazın:

import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ProductList } from "./ProductList";
import type { Product } from "./types";

const products: Product[] = [
  { id: 1, name: "Kablosuz Kulaklık", price: 1299, inStock: true },
  { id: 2, name: "Mekanik Klavye", price: 2199, inStock: true },
  { id: 3, name: "Oyuncu Faresi", price: 899, inStock: false },
];

function renderList(
  loadProducts: () => Promise<Product[]> = () => Promise.resolve(products),
  onAddToCart = vi.fn(),
) {
  render(<ProductList loadProducts={loadProducts} onAddToCart={onAddToCart} />);

  return { onAddToCart };
}

Adım 1: Yükleme

Konu: getBy*, findBy* ve queryBy*.3

test("yüklenirken bilgi mesajı, sonra ürünleri gösterir", async () => {
  renderList();

  expect(screen.getByText("Ürünler yükleniyor...")).toBeInTheDocument();
  expect(await screen.findAllByRole("article")).toHaveLength(3);
  expect(screen.queryByText("Ürünler yükleniyor...")).not.toBeInTheDocument();
});

Adım 2: Hata durumu

Konu: Promise.reject ve findByRole("alert").

test("yükleme başarısız olursa hata mesajı gösterir", async () => {
  renderList(() => Promise.reject(new Error("ağ hatası")));

  expect(await screen.findByRole("alert")).toHaveTextContent(
    "Ürünler yüklenemedi.",
  );
});

Adım 3: Arama ve boş durum

Konu: userEvent.type ve userEvent.clear.

test("aramaya göre ürünleri filtreler", async () => {
  const user = userEvent.setup();
  renderList();

  await user.type(await screen.findByLabelText("Ürün ara"), "klavye");

  expect(screen.getAllByRole("article")).toHaveLength(1);
  expect(
    screen.getByRole("heading", { name: "Mekanik Klavye" }),
  ).toBeInTheDocument();
});

test("eşleşme yoksa boş durum mesajı gösterir", async () => {
  const user = userEvent.setup();
  renderList();

  await user.type(await screen.findByLabelText("Ürün ara"), "monitör");

  expect(screen.getByText("Ürün bulunamadı.")).toBeInTheDocument();
});

Adım 4: Sepete ekleme

Konu: within ile kart içinde arama.4

test("seçilen ürünü sepete ekler", async () => {
  const user = userEvent.setup();
  const { onAddToCart } = renderList();

  const card = await screen.findByRole("article", { name: "Mekanik Klavye" });
  await user.click(within(card).getByRole("button", { name: "Sepete ekle" }));

  expect(onAddToCart).toHaveBeenCalledWith(products[1]);
});

test("stokta olmayan ürünün butonu devre dışıdır", async () => {
  renderList();

  const card = await screen.findByRole("article", { name: "Oyuncu Faresi" });

  expect(
    within(card).getByRole("button", { name: "Sepete ekle" }),
  ).toBeDisabled();
});

Adım 5: Yükleme mesajının kaybolması

Konu: waitFor ve queryByRole.5

test("yükleme bitince durum mesajı kaybolur", async () => {
  renderList();

  await waitFor(() =>
    expect(screen.queryByRole("status")).not.toBeInTheDocument(),
  );
});

Hangi konuyu tekrar ettiniz?

AdımTekrar edilen konu
1Sorgu ailesi ve findAllByRole3
2Asenkron hata durumu
3userEvent.type, boş durum
4within, toBeDisabled4
5waitFor ile kaybolan eleman5

Tip

Her testte gerçek API yerine kendi loadProducts fonksiyonunuzu verdiniz. Mock kütüphanesi gerekmedi. API'yi mock'lamayı Mock adımında öğrenirsiniz.

Kendin dene

Listeye fiyat sıralaması ekleyin ve test edin:

  • ProductList içine Sırala etiketli bir <select> ekleyin. Seçenekler: Önerilen ve Fiyat: artan.

  • user.selectOptions ile Fiyat: artan seçeneğini seçin.

  • İlk kartın en ucuz ürün (Oyuncu Faresi) olduğunu test edin. İpucu: getAllByRole("article")[0].

Kaynaklar

  • 1DocsReact: You Might Not Need an Effect

    Veri çekerken yarış durumu ve eski cevapları yok sayan `ignore` bayrağı.

  • 2DocsMDN: String.prototype.toLocaleLowerCase()

    Türkçe gibi dillerde yerel büyük-küçük harf eşlemesi.

  • 3DocsTesting Library: About Queries

    `getBy`, `queryBy` ve `findBy` farkı; yokluk ve bekleme için hangisi kullanılır.

  • 4DocsTesting Library: within

    Sorguyu tek bir elemanın içine daraltma.

  • 5DocsTesting Library: Async API'leri

    `findBy` ile `waitFor` ilişkisi, 1000 ms zaman aşımı ve yeniden denemenin hata fırlatmaya bağlı olması.

Edit this page

Last updated: October 14, 2026

Previous step07. Asenkron TestlerNext step09. Mock