ViraStack
  • Akademi
  • Hakkında
  • Destek
Genel BakışKaynaklarKatkı
ViraStack

Frontend geliştiricileri için DX odaklı araç seti: starter’lar, ajan kuralları, form hook’ları ve rehber.

Ürünler

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

Keşfet

  • Akademi
  • Hakkında
  • Marka
  • Destek

© 2026 ViraStack.

Açık kaynak · MIT

Vitest

04 / 12Proje

Ürün Kartı

E-ticaret ürün kartı için adım adım test.

Senaryo

Mağaza sayfasında her ürün bir kartta görünür. Kart şunları yapar:

  • Ürün adını ve fiyatını gösterir.

  • Sepete ekle butonuna tıklanınca ürünü bildirir.

  • Ürün stokta yoksa Stokta yok yazar ve butonu devre dışı bırakır.

  • Ürün indirimliyse eski fiyatı üstü çizili gösterir.

Component

Bu proje boyunca kullanacağınız son ProductCard sürümü. Önceki sürüme oldPrice ve aria-labelledby eklendi.1 Bu iki ekleme sonraki derslerde işe yarar.

import { formatPrice } from "./format-price";
import type { Product } from "./types";

type ProductCardProps = {
  product: Product;
  onAddToCart: (product: Product) => void;
};

export function ProductCard({ product, onAddToCart }: ProductCardProps) {
  const titleId = `product-${product.id}-title`;

  return (
    <article aria-labelledby={titleId}>
      <h2 id={titleId}>{product.name}</h2>
      {product.oldPrice ? <s>{formatPrice(product.oldPrice)}</s> : null}
      <p>{formatPrice(product.price)}</p>
      {product.inStock ? null : <p>Stokta yok</p>}
      <button
        type="button"
        disabled={!product.inStock}
        onClick={() => onAddToCart(product)}
      >
        Sepete ekle
      </button>
    </article>
  );
}

Product tipine isteğe bağlı bir oldPrice alanı ekleyin:

export type Product = {
  id: number;
  name: string;
  price: number;
  oldPrice?: number;
  inStock: boolean;
};

Testler

Testleri tek bir dosyada, adım adım yazın. Önce ortak veriyi tanımlayın:

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

const headphones: Product = {
  id: 1,
  name: "Kablosuz Kulaklık",
  price: 1299,
  inStock: true,
};

Adım 1: Ürün bilgisi

describe("ProductCard", () => {
  test("ürün adını ve fiyatını gösterir", () => {
    render(<ProductCard product={headphones} onAddToCart={vi.fn()} />);

    expect(
      screen.getByRole("heading", { name: "Kablosuz Kulaklık" }),
    ).toBeInTheDocument();
    expect(screen.getByText("₺1.299,00")).toBeInTheDocument();
  });
});

Sonraki üç testi aynı describe bloğunun içine ekleyin.

Adım 2: Sepete ekleme

test("Sepete ekle butonuna tıklayınca ürünü bildirir", async () => {
  const user = userEvent.setup();
  const onAddToCart = vi.fn();
  render(<ProductCard product={headphones} onAddToCart={onAddToCart} />);

  await user.click(screen.getByRole("button", { name: "Sepete ekle" }));

  expect(onAddToCart).toHaveBeenCalledWith(headphones);
});

Adım 3: Stokta olmayan ürün

test("stokta olmayan ürün için butonu devre dışı bırakır", async () => {
  const user = userEvent.setup();
  const onAddToCart = vi.fn();
  render(
    <ProductCard
      product={{ ...headphones, inStock: false }}
      onAddToCart={onAddToCart}
    />,
  );

  expect(screen.getByText("Stokta yok")).toBeVisible();
  expect(screen.getByRole("button", { name: "Sepete ekle" })).toBeDisabled();

  await user.click(screen.getByRole("button", { name: "Sepete ekle" }));

  expect(onAddToCart).not.toHaveBeenCalled();
});

Adım 4: İndirimli fiyat

test("indirimli üründe eski fiyatı gösterir", () => {
  render(
    <ProductCard
      product={{ ...headphones, oldPrice: 1499 }}
      onAddToCart={vi.fn()}
    />,
  );

  expect(screen.getByText("₺1.499,00")).toBeInTheDocument();
  expect(screen.getByText("₺1.299,00")).toBeInTheDocument();
});

İpucu

userEvent tarayıcı gibi davranır. Devre dışı butona tıklamak onAddToCart fonksiyonunu çağırmaz.2

Hangi konuyu tekrar ettiniz?

AdımTekrar edilen konu
1render, screen, getByRole, toBeInTheDocument3
2userEvent, vi.fn, toHaveBeenCalledWith2
3toBeVisible, toBeDisabled, not.toHaveBeenCalled4
4getByText ve ortak test verisi

  • Fiyat metnini formatPrice ile üretmeyin. Testte "₺1.299,00" gibi sabit metin yazın. Biçim bozulursa test başarısız olur ve hatayı gösterir.

  • Fiyat biçimi Intl için kullanılan ICU verisine bağlıdır. Bu veri Node.js derlemesine ve sisteme göre değişebilir. Takımınız farklı ortamlar kullanıyorsa CI ortamında bir kez çalıştırarak doğrulayın.5

Kendin dene

Önce testi yazın, sonra component'i değiştirin (TDD). İndirimli ürün için yüzde bilgisi eklenecek.

  • Test: price 1299, oldPrice 1499 olan ürün kartında %13 indirim yazısı görünür.

  • Testin başarısız olduğunu görün.

  • ProductCard içine yüzdeyi hesaplayıp gösterin.

  • Testin geçtiğini görün.

Kaynaklar

  • 1DokümanWAI-ARIA 1.2: Roles

    `article` rolünün adı içerikten değil yazardan gelir, bu yüzden `aria-labelledby` gerekir.

  • 2Dokümanuser-event: Giriş

    `userEvent` tarayıcının izin vermediği etkileşimi, örneğin devre dışı alanı, yapmaz.

  • 3DokümanReact Testing Library: Örnek

    `render`, `screen` ve rol sorgularıyla örnek test.

  • 4Repojest-dom

    `toBeVisible` ve `toBeDisabled` matcher'larının kuralları.

  • 5DokümanNode.js: Internationalization support

    `Intl` çıktısının ICU verisine ve Node.js derlemesine bağlılığı.

Bu sayfayı düzenle

Son güncelleme tarihi: 14 Ekim 2026

Önceki adım03. TemellerSonraki adım05. Sorgular