# Playwrite

## E2E(End to End) Test

> 실제 사용자가 사용하는 브라우저 환경에서 애플리케이션 전체를 동작해서 기능을 확인하는 테스트
>
> 즉, 사용자가 실제로 소프트웨어를 사용했을때 플로우를 테스트하는 것

### E2E Test 특징

* 실제 사용자 환경에서 테스트하므로 사용자의 입장에서의 경험을 직접적으로 테스트할 수 있다.
* 전체 시스템을 대상으로 테스트를 수행하기 때문에 각각의 모듈이나 컴포넌트가 함께 작동할 때 발생하는 문제점들을 파악 가능
* 외부 환경이나 네트워크를 이용한다면 조금 불안정 할 수 있다.

## E2E Test Framework

### Cypress

```bash
npm i -D cypress
```

```bash
npx cypress open
```

`Cypress`의 예제 코드를 살펴보면 UI 요소를 가져올 때 내부 구현 사항 (예: class)에 의존하고 있다.

testing-library는 Cypress도 지원하므로 함께 사용하면 UI 요소를 사용자 관점에서 가져올 수 있다

```bash
npm i -D @testing-library/cypress
```

E2E 테스트에서도 마찬가지로, 네트워크 관련 의존성들은 Mock 데이터를 활용하여 테스트를 진행한다.

이는 사용자의 실제 환경과 유사한 환경에서 테스트를 수행하더라도 외부 의존성에 의해 테스트가 실패하는 것을 방지하기 위함이다.

```typescript
import '@testing-library/cypress/add-commands'

describe('App', () => {
  beforeEach(() => {
    cy.intercept('GET', '/restaurants', {
      fixture: 'restaurants.json', 
    })
    
    cy.visit('http://localhost:3001/');
  })

  it('display text search in the header', () => {
    cy.findByText(/search/).should('exist');
  })
})
```

### Headless Chrome

> Headless 상태에서도 크롬 브라우저를 실행할 수 있는 방법, 크롬없이 크롬을 실행하는 것이라고 설명하는데  크롤링을 하거나 E2E 테스트할 때 유용하다고 한다.

Chrome 브라우저의 기능을 그대로 가지고 있지만, 실제로 화면을 그리지 않고 내부적으로만 실행되는 모드\
즉, GUI -> CLI

{% hint style="info" %}
**headless란?**

Headless는 일반적으로 **사용자 인터페이스 (UI)가 없는 응용 프로그램**을 의미

"웹 개발에서는 브라우저에 UI를 렌더하지 않고 탐색하는브라우저 모드를 의미하기도 한다.\
이 모드에서는 웹 사이트를 브라우저 창이나 탭 없이 백그라운드에서 실행할 수 있으며, 대개 자동화된 웹 테스트나 웹 스크래핑 작업 등에서 사용된다"

* 일반적으로 headless로 동작하면 화면에 UI를 렌더링하지 않으므로 테스트 실행속도가 더 빠르다.
* CI/CD 파이프라인에서 자동화된 테스트를 수행할 때 유용하다.
  {% endhint %}

### Puppeteer

> Google에서 개발한 Node.js용 Headless Chrome 라이브러리

**Puppeteer**는 Headless Chrome을 자바스크립트로 제어하여 웹 크롤링, 스크린샷, E2E 테스트 등 다양한 작업을 자동화할 수 있다.

`Chrome DevTools Protocol`을 사용하여 Chrome 브라우저의 인스턴스를 제어하며, 이를 통해 Chrome 브라우저의 모든 기능을 사용할 수 있다.

### [CodeceptJS](https://github.com/codeceptjs/create-codeceptjs)

> E2E 테스트 도구로 보다 간단하고 테스트 코드 작성을 비개발자도 쉽게 이해할 수 있도록 직관적인 API를 제공한다.&#x20;

#### 장점

* 가독성이 높은 DSL(Domain Specific Language)을 제공하여 테스트 코드 작성이 용이함
* Selenium, Puppeteer, Playwright 등, 다양한 드라이버를 제공하여 다른 도구로 작성된 테스트 코드를 `CodeceptJS` 로 변환이 가능하다.

#### Getting Started

```bash
npx create-codeceptjs .
```

{% hint style="info" %}
해당 명령어를 입력하면 package.json 스페이스가 4칸으로 바뀌게 된다. 아래 명령어를 통해 복구!

```bash
sed "s/    /  /g" package.json > package.json.tmp
rm package.json
mv package.json.tmp package.json
```

{% endhint %}

`package.json` 에 script가 자동으로 추가된다.

```json
"scripts": {
  // 웹 브라우저를 화면에 띄워 테스트를 실행합니다. --steps: 각 단계의 실행 결과가 자세히 표시
  "codeceptjs": "codeceptjs run --steps",
  // 웹 브라우저를 화면에 띄우지 않고 테스트를 실행
  "codeceptjs:headless": "HEADLESS=true codeceptjs run --steps",
  // 웹 브라우저에 CodeceptUI를 띄워 훨씬 편하게 테스트를 실행합니다.
  "codeceptjs:ui": "codecept-ui --app",
  // demo는 기능과 사용법을 익히기 위해 codeceptjs가 제공하는 샘플 시나리오를 실행하는것
  "codeceptjs:demo": "codeceptjs run --steps -c node_modules/@codeceptjs/examples",
  "codeceptjs:demo:headless": "HEADLESS=true codeceptjs run --steps -c node_modules/@codeceptjs/examples",
  "codeceptjs:demo:ui": "codecept-ui --app  -c node_modules/@codeceptjs/examples"
}
```

<pre class="language-bash"><code class="lang-bash">npm uninstall @codeceptjs/examples <a data-footnote-ref href="#user-content-fn-1">// 예제 코드 제거</a>
</code></pre>

```bash
npx codeceptjs init

? Where are your tests located? (./*_test.js)
# => ./tests/**/*_test.js

? What helpers do you want to use?
(Use arrow keys)
# => ❯ Playwright

? Where should logs, screenshots, and reports to be stored? (./output)
# => ./output

? Do you want localization for tests? (See https://codecept.io/translation/)
(Use arrow keys)
# => ❯ English (no localization)

? [Playwright] Base url of site to be tested (http://localhost)
# => http://localhost:8080

? [Playwright] Show browser window (Y/n)
# => Y

? [Playwright] Browser in which testing will be performed.
Possible options: chromium, firefox or webkit (chromium)
# => chromium

Creating a new test...
----------------------

? Feature which is being tested (ex: account, login, etc)
# => google

? Filename of a test (google_test.js)
# => google_test.js
```

#### 샘플 코드

```typescript
Feature('Google');

Scenario('Search “CodeceptJS”', ({ I }) => {
  I.amOnPage('https://www.google.com/ncr');
  I.fillField('[name="q"]', 'CodeceptJS');
  I.click('Google Search');
  I.see('codecept.io');
});
```

#### 실행

```bash
npm run codeceptjs
```

## Playwright

> Headless Chrome을 기반으로 한 Puppteer을 계승하면서 더 많은 웹 브라우저를 지원한다.

```bash
npm i -D @playwright/test eslint-plugin-playwright
```

```bash
npx playwright test
```

### 환경설정

`playwright.config.ts`

```typescript
import { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
  testDir: './tests',
  retries: 0,
  use: {
    channel: 'chrome',
    baseURL: 'http://localhost:8080', // 도메인 URL 설정
    headless: !!process.env.CI, // CI 환경에선 headless로 동작 
    screenshot: 'only-on-failure', // 테스트 실패할 때 스크린샷을 찍어 남김
  },
};

export default config;
```

{% hint style="info" %}
.gitignore 파일에 에러 상황의 스크린샷 등이 담기는 `test-results/` 디렉터리 추가.
{% endhint %}

`tests/.eslintrc.js`

```typescript
module.exports = {
  env: {
    jest: false, // jest를 사용하지 않고 playwright에서 제공하는 api를 사용하므로 false
  },
  extends: ['plugin:playwright/playwright-test'],
  rules: {
    'import/no-extraneous-dependencies': 'off',
  },
}
```

`jest.config.js`

E2E 테스트는 .spec, 단위 테스트는 .test로 구분하여 단위테스트 실행시 E2E 테스트는 실행하지 않도록 설정

```javascript
module.exports = {
  //...
  testMatch: ['**/*.test.{ts,tsx}', '!**/*.spec.{ts,tsx}'],
  // 또는
  testPathIgnorePatterns: ['<rootDir>/e2e/'],
};
```

### 테스트 코드 예시

UI가 복잡해지면 사용자 관점에서 특정 요소를 지정하는 것이 아직은 어려운데 추후에 알게되면 해당 내용을 반영해서 다시 작성할 예정

```typescript
import { test, expect } from '@playwright/test';

test('Filter Food', async ({ page }) => {
  await page.goto('/');

  await expect(page.getByRole('heading', { name: '식당 검색창' })).toBeVisible();

  const searchInput = page.getByLabel('검색');

  await searchInput.fill('메리김밥');

  await expect(page.getByText('제육김밥')).toBeVisible();

  await searchInput.fill('메가반점');

  await expect(page.getByText('짜장면')).toBeVisible();

  await searchInput.fill('아무거나');

  await expect(page.getByText('짜장면')).toBeHidden();

  const radioBtn = page.getByRole('radio', { name: '한식' });

  await radioBtn.click();

  await expect(page.getByText('메리김밥')).toBeVisible();

  await expect(page.getByText('메가반점')).toBeHidden();
});

test('Cart', async ({ page }) => {
  await page.goto('/');

  const addBtn = await page.waitForSelector('button[name="#짜장면"]');

  await addBtn.click();
  await addBtn.click();

  await expect(page.getByText('합계: 8000원 주문')).toBeVisible();

  const removeBtn = page.getByRole('button', { name: '짜장면제거' });

  await removeBtn.click();

  await expect(page.getByText('합계: 0원 주문')).toBeVisible();
});

test('When order complete display receipt ', async ({ page }) => {
  await page.goto('/');

  const addBtn = await (page.waitForSelector('button[name="#짜장면"]'));

  await addBtn.click();

  const orderBtn = page.getByRole('button', { name: '주문하기' });

  await orderBtn.click();

  await expect(page.getByRole('table', { name: '영수증' })).toBeVisible();
});

test('Receipts disappear after 5 seconds', async ({ page }) => {
  await page.goto('/');

  const addBtn = await page.waitForSelector('button[name="#짜장면"]');

  await addBtn.click();

  const orderBtn = page.getByRole('button', { name: '주문하기' });

  await orderBtn.click();

  // eslint-disable-next-line playwright/no-wait-for-timeout
  await page.waitForTimeout(5000);

  await expect(page.getByRole('table', { name: '영수증' })).toBeHidden();
  await expect(page.getByRole('heading', { name: '[영수증 나오는 곳]' })).toBeVisible();
});
```

{% hint style="info" %}
`waitFor*`와 같은 특정 시간을 기다리는 API를 사용할 때, 이전에 `expect()를 작성했다면 동작하지 않는다.`
{% endhint %}

[^1]:


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://taewoongs-organization.gitbook.io/jtwjs-dev-wiki/dev_road/week-5/playwrite.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
