Skip to content

Commit

Permalink
Feature #191: playwright 적용해서 e2e 테스트하기 (#203)
Browse files Browse the repository at this point in the history
* feat(#191): playwright 설치하기

* feat(#191): playwright로 간단히 테스트해보기

* refactor(#191): mock 변경 사항 반영 및 불필요한 resolver 제거 및 resolver 함수명 통일하기

* refactor(#191): mock history 불필요한 함수 제거 및 DTO 적용하기

* refactor(#191): mock lotus 제목 검색 로직 추가하기

* refactor(#191): mocking 시에 페이지네이션 max 값도 계산하기

* refactor(#191): user에 대한 MockRepository 적용하기

* refactor(#191): 테스트를 위한 data-testid값을 부여

* fix(#191): lotus 삭제 시 오류 수정하기

* refactor(#191): app 레이어로 테스트 코드 이동 및 e2e 테스트 코드 작성완료
  • Loading branch information
naarang authored Dec 4, 2024
1 parent adbcdc5 commit 8519406
Show file tree
Hide file tree
Showing 18 changed files with 539 additions and 400 deletions.
4 changes: 4 additions & 0 deletions apps/frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
10 changes: 8 additions & 2 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest"
"test": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report"
},
"lint-staged": {
"*.{ts,tsx}": [
Expand All @@ -33,11 +37,13 @@
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@playwright/test": "^1.49.0",
"@tanstack/react-query-devtools": "^5.59.19",
"@tanstack/router-devtools": "^1.78.3",
"@tanstack/router-plugin": "^1.78.3",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/node": "^20.3.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/parser": "^5.59.11",
Expand Down Expand Up @@ -67,4 +73,4 @@
"public"
]
}
}
}
35 changes: 35 additions & 0 deletions apps/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './src/app/test/e2e',
fullyParallel: true,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry'
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],

/* Run your local dev server before starting the tests */
webServer: {
command: 'pnpm dev',
url: 'http://localhost:5173'
}
});
36 changes: 32 additions & 4 deletions apps/frontend/src/app/mock/MockRepository/MockRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,36 @@ export class MockRepository<T> {
return true;
}

private isPartialMatch(owner: Partial<T>, target: Partial<T>): boolean {
for (const key in target) {
if (!Object.prototype.hasOwnProperty.call(owner, key)) return false;

const ownerValue = owner[key as keyof T];
const targetValue = target[key as keyof T];

if (typeof ownerValue === 'boolean' && ownerValue !== targetValue) {
return false;
}

if (typeof targetValue === 'string' && !(ownerValue as string)?.includes(targetValue)) {
return false;
}
}
return true;
}

private generateId() {
return String(this._autoId++);
}

private paginate(items: Identifiable<T>[], page: number, size: number) {
const start = (page - 1) * size;
const end = start + size;
const data = items.slice(start, end);
const maxPage = Math.ceil(items.length / size);
return { data, maxPage };
}

async create(arg: T) {
const data = { ...arg, id: this.generateId() };

Expand Down Expand Up @@ -52,10 +78,7 @@ export class MockRepository<T> {

async findMany({ query, page = 1, size = 10 }: { query?: Partial<Identifiable<T>>; page?: number; size?: number }) {
const filtered = query ? this.memory.filter((item) => this.isMatch(item, query)) : this.memory;
const start = (page - 1) * size;
const end = start + size;

return filtered.slice(start, end);
return this.paginate(filtered, page, size);
}

async findOne(query: Partial<Identifiable<T>>) {
Expand All @@ -65,4 +88,9 @@ export class MockRepository<T> {

return data;
}

async search({ query, page = 1, size = 10 }: { query?: Partial<Identifiable<T>>; page?: number; size?: number }) {
const filtered = query ? this.memory.filter((item) => this.isPartialMatch(item, query)) : this.memory;
return this.paginate(filtered, page, size);
}
}
4 changes: 2 additions & 2 deletions apps/frontend/src/app/mock/gistResolvers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpResponse, PathParams } from 'msw';

// 사용자의 Gist 목록 조회
export const mockGetUserGistList = () => {
export const getUserGistList = () => {
return HttpResponse.json({
gists: [
{
Expand All @@ -27,7 +27,7 @@ export const mockGetUserGistList = () => {
};

// 특정 Gist 파일 조회 api
export const mockGetGistDetail = ({ params }: { params: PathParams }) => {
export const getGistDetail = ({ params }: { params: PathParams }) => {
const { gistId } = params;

if (!gistId) {
Expand Down
57 changes: 28 additions & 29 deletions apps/frontend/src/app/mock/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
import { http } from 'msw';
import { mockGetGistDetail, mockGetUserGistList } from './gistResolvers';
import { getGistDetail, getUserGistList } from './gistResolvers';
import { getHistory, getHistoryList, getTagList, postCodeRun, postTag } from './historyResolvers';
import {
mockGetHistory,
mockGetHistoryList,
mockGetTagList,
mockGetUserLotusList,
mockPostCodeRun,
mockPostTag
} from './historyResolvers';
import { deleteLotus, getLotusDetail, getPublicLotusList, patchLotus, postCreateLotus } from './lotusResolvers';
import { mockGetLogin, mockGetUserInfo, mockLogin, mockLogout, mockPatchUserInfo } from './userResolvers';
deleteLotus,
getLotusDetail,
getPublicLotusList,
getUserLotusList,
patchLotus,
postCreateLotus
} from './lotusResolvers';
import { getLogin, getUserInfo, patchUserInfo } from './userResolvers';

const apiUrl = (path: string) => `${import.meta.env.VITE_API_URL}${path}`;

export const handlers = [
// user
http.get(`/api/user`, mockGetUserInfo),
http.patch(`/api/user`, mockPatchUserInfo),
http.post(`/api/user/login`, mockLogin),
http.get(`/api/user/login`, mockGetLogin),
http.post(`/api/user/logout`, mockLogout),
http.get(`/api/user/lotus`, mockGetUserLotusList),
http.get(`/api/user/gist`, mockGetUserGistList),
http.get(`/api/user/gist/:gistId`, mockGetGistDetail),
http.get(apiUrl(`/api/user`), getUserInfo),
http.patch(apiUrl(`/api/user`), patchUserInfo),
http.get(apiUrl(`/api/user/login/callback`), getLogin),
http.get(apiUrl(`/api/user/lotus`), getUserLotusList),
http.get(apiUrl(`/api/user/gist`), getUserGistList),
http.get(apiUrl(`/api/user/gist/:gistId`), getGistDetail),
// lotus
http.get(`/api/lotus`, getPublicLotusList),
http.get(`/api/lotus/:lotusId`, getLotusDetail),
http.post(`/api/lotus`, postCreateLotus),
http.patch(`/api/lotus/:id`, patchLotus),
http.delete(`/api/lotus/:id`, deleteLotus),
http.get(apiUrl(`/api/lotus`), getPublicLotusList),
http.get(apiUrl(`/api/lotus/:lotusId`), getLotusDetail),
http.post(apiUrl(`/api/lotus`), postCreateLotus),
http.patch(apiUrl(`/api/lotus/:id`), patchLotus),
http.delete(apiUrl(`/api/lotus/:id`), deleteLotus),
// history
http.get(`/api/lotus/:lotusId/history`, mockGetHistoryList),
http.post(`/api/lotus/:lotusId/history`, mockPostCodeRun),
http.get(`/api/lotus/:lotusId/history/:historyId`, mockGetHistory),

http.get(apiUrl(`/api/lotus/:lotusId/history`), getHistoryList),
http.post(apiUrl(`/api/lotus/:lotusId/history`), postCodeRun),
http.get(apiUrl(`/api/lotus/:lotusId/history/:historyId`), getHistory),
// tag
http.get(`/api/tag`, mockGetTagList),
http.post(`/api/tag`, mockPostTag)
http.get(apiUrl(`/api/tag`), getTagList),
http.post(apiUrl(`/api/tag`), postTag)
];
Loading

0 comments on commit 8519406

Please sign in to comment.