08 / 12Proje
Ü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?
İpucu
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:
ProductListiçine Sırala etiketli bir<select>ekleyin. Seçenekler: Önerilen ve Fiyat: artan.user.selectOptionsile 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
Veri çekerken yarış durumu ve eski cevapları yok sayan `ignore` bayrağı.
Türkçe gibi dillerde yerel büyük-küçük harf eşlemesi.
`getBy`, `queryBy` ve `findBy` farkı; yokluk ve bekleme için hangisi kullanılır.
Sorguyu tek bir elemanın içine daraltma.
`findBy` ile `waitFor` ilişkisi, 1000 ms zaman aşımı ve yeniden denemenin hata fırlatmaya bağlı olması.
Son güncelleme tarihi: 14 Ekim 2026
