Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

핵심기능 E2E 테스트 - 카테고리, 검색 #648

Closed
wants to merge 9 commits into from
29 changes: 29 additions & 0 deletions frontend/e2eTests/category.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Page } from '@playwright/test';

interface Props {
page: Page;
categoryName: string;
}

export const getCategoryButton = ({ page, categoryName }: Props) => page.getByRole('button', { name: categoryName });

export const createCategory = async ({ page, categoryName }: Props) => {
await page.getByRole('button', { name: '카테고리 편집' }).click();

await page.getByRole('button', { name: '+ 카테고리 추가' }).click();
await page.getByPlaceholder('카테고리 입력').fill(categoryName);
await page.getByPlaceholder('카테고리 입력').press('Enter');

await page.getByRole('button', { name: '저장' }).click();
};

export const deleteCategory = async ({ page, categoryName }: Props) => {
await page.getByRole('button', { name: '카테고리 편집' }).click();

const categoryInEditModal = page.getByText(categoryName).nth(1);

await categoryInEditModal.hover();
await page.getByRole('button', { name: '카테고리 삭제' }).click();

await page.getByRole('button', { name: '저장' }).click();
};
95 changes: 95 additions & 0 deletions frontend/e2eTests/category.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { test, expect } from '@playwright/test';

import { createCategory, deleteCategory, getCategoryButton } from './category.actions';
import { loginToCodezap, waitForSuccess } from './utils';

test.beforeEach(async ({ page }) => {
await loginToCodezap({
page,
id: process.env.PLAYWRIGHT_TEST_ID || '',
password: process.env.PLAYWRIGHT_TEST_PASSWORD || '',
});
});

test('카테고리 편집 모달에서 새 카테고리를 추가 및 삭제할 수 있다.', async ({ page, browserName }) => {
const newCategoryName = `생성테스트-${browserName}`;

await createCategory({ page, categoryName: newCategoryName });

await waitForSuccess({ page, apiUrl: '/categories' });

const newCategoryButton = getCategoryButton({ page, categoryName: newCategoryName });

await expect(newCategoryButton).toBeVisible();

// 다음 테스트를 위해 테스트용 카테고리 삭제
await deleteCategory({ page, categoryName: newCategoryName });

await waitForSuccess({ page, apiUrl: '/categories' });

await expect(newCategoryButton).not.toBeVisible();
});

test('카테고리 편집 모달에서 카테고리명을 수정 및 삭제할 수 있다.', async ({ page, browserName }) => {
const newCategoryName = `수정테스트-${browserName}`;
const editedCategoryName = `수정완료-${browserName}`;

// 수정할 카테고리 생성
await createCategory({ page, categoryName: newCategoryName });

await waitForSuccess({ page, apiUrl: '/categories' });

const newCategoryButton = getCategoryButton({ page, categoryName: newCategoryName });

await expect(newCategoryButton).toBeVisible();

// 카테고리 수정
await page.getByRole('button', { name: '카테고리 편집' }).click();

const newCategoryInEditModal = page.getByText(newCategoryName).nth(1);

await newCategoryInEditModal.hover();
await page.getByRole('button', { name: '카테고리 이름 변경' }).click();
await page.getByPlaceholder('카테고리 입력').click();
await page.getByPlaceholder('카테고리 입력').fill(editedCategoryName);
await page.getByRole('button', { name: '저장' }).click();

const editedCategoryButton = getCategoryButton({ page, categoryName: editedCategoryName });

await expect(editedCategoryButton).toBeVisible();

// 다음 테스트를 위해 테스트용 카테고리 삭제
await deleteCategory({ page, categoryName: editedCategoryName });

await waitForSuccess({ page, apiUrl: '/categories' });
await expect(editedCategoryButton).not.toBeVisible();
});

test('카테고리는 최대 15글자까지만 입력할 수 있다.', async ({ page, browserName }) => {
const rawCategoryName = `최대글자수테스트-${browserName}`;
const expectedCategoryName = rawCategoryName.slice(0, 15);

await page.getByRole('button', { name: '카테고리 편집' }).click();
await page.getByRole('button', { name: '+ 카테고리 추가' }).click();
const categoryInput = page.getByPlaceholder('카테고리 입력');

await categoryInput.click();

for (const char of rawCategoryName) {
await page.keyboard.type(char);
}
Comment on lines +78 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 Locator.fill()로 입력하지 않고 page.keyboard.type으로 입력한 이유가 있나요? 다음 링크의 caution에서는 fill을 권장하는 것 같아서요!
https://playwright.dev/docs/api/class-keyboard#keyboard-type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locator.fill()은 복사붙여넣기 처럼 동작하기 때문에 16자 이상의 글자를 fill로 넣으면 아무것도 입력되지않아 해당 테스트에서 실패하였습니다. 🤣

(16자 이상의 글자를 복사붙여넣기하면 아예 복사붙여넣기가 되지 않는데, 그 이유는 validate 함수에서 아예 16자 이상의 입력을 막고있기 때문인 것 같습니다.)

따라서 15자 이후에 계속 입력하여도 15자까지만 카테고리가 생성된다는 것을 테스트하기 위해 page.keyboard.type를 사용하였습니다!


await page.getByRole('button', { name: '저장' }).click();

await waitForSuccess({ page, apiUrl: '/categories' });

const newCategoryButton = getCategoryButton({ page, categoryName: expectedCategoryName });

await expect(newCategoryButton).toBeVisible();

// 다음 테스트를 위해 테스트용 카테고리 삭제
await deleteCategory({ page, categoryName: expectedCategoryName });

await waitForSuccess({ page, apiUrl: '/categories' });
await expect(newCategoryButton).not.toBeVisible();
});
15 changes: 0 additions & 15 deletions frontend/e2eTests/example.spec.ts

This file was deleted.

15 changes: 15 additions & 0 deletions frontend/e2eTests/search.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Page } from '@playwright/test';

interface Props {
page: Page;
keyword: string;
}

export const searchTemplates = async ({ page, keyword }: Props) => {
const searchInput = page.getByPlaceholder('검색');

await searchInput.waitFor({ state: 'visible' });
await searchInput.click();
await searchInput.fill(keyword);
await searchInput.press('Enter');
};
30 changes: 30 additions & 0 deletions frontend/e2eTests/search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test, expect } from '@playwright/test';

import { searchTemplates } from './search.actions';
import { loginToCodezap, waitForSuccess } from './utils';

test.beforeEach(async ({ page }) => {
await loginToCodezap({
page,
id: process.env.PLAYWRIGHT_TEST_ID || '',
password: process.env.PLAYWRIGHT_TEST_PASSWORD || '',
});
});

test('검색창에 `테스트`를 입력하면 `테스트`가 내용에 포함된 템플릿 목록을 확인할 수 있다.', async ({ page }) => {
const keyword = '테스트';

await searchTemplates({ page, keyword });

await waitForSuccess({ page, apiUrl: '/templates?keyword' });
await expect(page.getByRole('link', { name: /테스트/ })).toBeVisible();
});

test('검색창에 `ㅁㅅㅌㅇ`를 입력할 경우 `검색 결과가 없습니다`가 나온다.', async ({ page }) => {
const keyword = 'ㅁㅅㅌㅇ';

await searchTemplates({ page, keyword });

await waitForSuccess({ page, apiUrl: '/templates?keyword' });
await expect(page.locator('div').filter({ hasText: /^검색 결과가 없습니다\.$/ })).toBeVisible();
});
32 changes: 32 additions & 0 deletions frontend/e2eTests/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Page } from '@playwright/test';

interface LoginToCodezapProps {
page: Page;
id: string;
password: string;
}

export const loginToCodezap = async ({ page, id, password }: LoginToCodezapProps) => {
await page.goto('/');
await page.getByRole('link', { name: '로그인', exact: true }).getByRole('button').click();
await page
.locator('div')
.filter({ hasText: /^아이디 \(닉네임\)$/ })
.locator('div')
.click();
await page.locator('input[type="text"]').fill(id);
await page.locator('input[type="text"]').press('Tab');
await page.locator('input[type="password"]').fill(password);
await page.locator('form').getByRole('button', { name: '로그인' }).click();

await waitForSuccess({ page, apiUrl: '/login' });
};

interface WaitForSuccessProps {
page: Page;
apiUrl: string;
}

export const waitForSuccess = async ({ page, apiUrl }: WaitForSuccessProps) => {
await page.waitForResponse((response) => response.url().includes(apiUrl) && response.status() === 200);
};
Loading