> For the complete documentation index, see [llms.txt](https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/dev_road/week-9/undefined-1.md).

# 상품 목록 페이지

## 상품 목록 보기 기능

> 상품 목록을 얻어서 표시하는 화면
>
> 1. 상품 목록 패칭
> 2. 상품 목록 리스팅

### User Scenario

* 카테고리 별로 상품 목록이 보여져야 한다.
* 카테고리를 선택하지 않으면 전체 상품이 노출된다.
* 각 상품은 상품명과 가격이 노출된다.
* 상품을 클릭하면 상품 상세 페이지로 넘어간다.

{% hint style="info" %}
[Null Object Pattern](https://refactoring.com/catalog/introduceSpecialCase.html)&#x20;

* 객체지향 디자인패턴 중 하나로 객체가 null 값을 반환할 때 생길 수 있는 문제를 해결하기 위해 사용됨
* NullPointerException 같은 예외를 피하기 위해 null 대신 객체의 인터페이스를 충족하지만 값이 없는 객체를 반환한다.
  {% endhint %}

{% hint style="info" %}
[Intl Object](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Intl)  다양한 나라의 언어에 맞는 문자열, 숫자, 시간, 날짜 비교와 관련된 포맷팅 기능을 제공한다.&#x20;
{% endhint %}

<details>

<summary>Unit Test</summary>

#### ProductList.test.tsx

```typescript
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Route, useParams } from "react-router-dom";

import { products } from "@/fixtures";
import { withAllContexts, withRouter } from "@/tests/utils";

import ProductList from "./ProductList";

const context = describe;

describe("ProductList", () => {
  it("render correctly", async () => {
    render(
      withAllContexts(withRouter(<Route path="/" element={<ProductList />} />))
    );

    await Promise.all(
      products.map(async (product) => {
        await waitFor(() =>
          expect(screen.getByText(product.name)).toBeInTheDocument()
        );
        await waitFor(() =>
          expect(screen.getByText(product.price)).toBeInTheDocument()
        );
      })
    );

    await waitFor(() => expect(screen.getAllByRole("link")).toHaveLength(2));
  });

  context("when product item is clicked", () => {
    const product = products[0];

    it("navigate to the product detail page", async () => {
      function DetailPage() {
        const params = useParams();
        return <pre>{JSON.stringify(params)}</pre>;
      }

      render(
        withAllContexts(
          withRouter(
            <>
              <Route path="/" element={<ProductList />} />
              <Route path="/:id" element={<DetailPage />} />
            </>
          )
        )
      );

      await waitFor(() => screen.getByText(product.name));
      const linkBtn = screen.getAllByRole("link")[0];

      userEvent.click(linkBtn);

      await waitFor(() =>
        expect(screen.getByText(new RegExp(product.id))).toBeInTheDocument()
      );
    });
  });
});
```

#### Product.test.tsx

```typescript
import { render, screen } from "@testing-library/react";

import { products } from "@/fixtures";
import { withAllContexts } from "@/tests/utils";

import Product from "./Product";

describe("Product", () => {
  const product = products[0];
  it("render correctly", () => {
    render(withAllContexts(<Product product={product} />));

    expect(screen.getByRole("img")).toHaveAttribute(
      "src",
      product.thumbnail.url
    );

    expect(screen.getByText(product.name)).toBeInTheDocument();
    expect(screen.getByText(product.price)).toBeInTheDocument();
  });
})

```

</details>

<details>

<summary>E2E Test</summary>

```typescript
/// <reference types="cypress" />
import "@testing-library/cypress/add-commands";

describe("Product List", () => {
  beforeEach(() => {
    cy.visit("http://localhost:8081");
  });

  context("when product link btn is clicked", () => {
    it("display products from all categories", () => {
      cy.findByRole("link", { name: "Products" }).click();

      cy.findByText(/CBCL 하트자수맨투맨/).should("exist");
      cy.findByText(/밴딩스커트/).should("exist");
      cy.findByText(/CBCL EARRING Silver/).should("exist");
    });
  });

  context("when product is clicked", () => {
    it("navigate product detail page", () => {
      cy.findByRole("link", { name: "Products" }).click();

      cy.findByRole("link", { name: /CBCL 하트자수맨투맨/ }).click();

      cy.findByText("상품 상세 페이지").should("exist");
    });
  });
});

```

</details>

## 상품 카테고리 목록

> &#x20;외부에서 상품 카테고리를 주입받아(API Fetching)  헤더에 표시

해당 데이터 관리를 어디서 하느냐에 따라 외부(API)에서 주입받는 데이터, 내부에서 관리하는 데이터로 구분할 수 있다.

즉, 어드민 페이지에서 상품 카테고리를 관리한다면 외부에서 데이터를 주입받아 표시하는 형태로 진행한다.

{% hint style="info" %}
자주 바뀌지 않고 클라이언트에서 즉시 수정하는게 편한 데이터는 클라이언트 내부에서 관리하는 것이 좋다.
{% endhint %}

### User Scenario

* 어드민에서 지정한 카테고리 목록들이 표시된다.
* 카테고리 버튼을 클릭하면 해당하는 상품 목록 페이지로 넘어간다.

<details>

<summary>Unit Test</summary>

```typescript
import { render, screen, waitFor } from "@testing-library/react";
import { Route } from "react-router-dom";

import { categories } from "@/fixtures";
import { withAllContexts, withRouter } from "@/tests/utils";

import Header from "./Header";

describe("Header", () => {
  function renderHeader() {
    return render(
      withAllContexts(withRouter(<Route path="/" element={<Header />} />))
    );
  }

  it("render correctly", () => {
    renderHeader();

    expect(
      screen.getByRole("heading", { level: 1, name: "Shop" })
    ).toBeInTheDocument();
    expect(screen.getByRole("link", { name: "Products" })).toBeInTheDocument();
    expect(screen.getByRole("link", { name: "Cart" })).toBeInTheDocument();
  });

  it("render categories", async () => {
    renderHeader();

    await Promise.all(
      categories.map(async (ctg) => {
        await waitFor(() =>
          expect(
            screen.getByRole("link", { name: ctg.name })
          ).toBeInTheDocument()
        );
      })
    );
  });
});
```

</details>

<details>

<summary>E2E Test</summary>

```typescript
/// <reference types="cypress" />
import "@testing-library/cypress/add-commands";

describe("Product List", () => {
  beforeEach(() => {
    cy.visit("http://localhost:8081");
  });
  
  context("when category btn is clicked", () => {
    it("display product from  categories", () => {
      cy.findByRole("link", { name: "top" }).click();

      cy.findByText(/밴딩스커트/).should("not.exist");
      cy.findByText(/CBCL 하트자수맨투맨/).should("exist");

      cy.findByRole("link", { name: "acc" }).click();

      cy.findByText(/CBCL 하트자수맨투맨/).should("not.exist");
      cy.findByText(/CBCL EARRING Silver/).should("exist");
      cy.findByText(/62000/).should("exist");
    });
  });
});

```

</details>
