test: Playwright E2E 자동화 도입 (14 스펙, 관리자/구성원 역할 커버)
All checks were successful
build-and-push / build (push) Successful in 36s
All checks were successful
build-and-push / build (push) Successful in 36s
- playwright.config.ts: 로컬 스택(5380/8380) 대상 headless chromium - e2e/spin.spec.ts: 대시보드·네비·쪽지함·캘린더 일정 생성·프로젝트 생성 모달·검증, 구성원 역할(관리자 메뉴 숨김·관리 라우트 리다이렉트·프로필) - e2e/project-flow.spec.ts: 프로젝트 상세 개요/작업자/메일 탭, 타임라인 작업 추가→상세 모달→칸반 카드 - e2e/member-domains.spec.ts: 근무·휴가 신청 검증, 인센티브 게이지, 대시보드 내 이슈 - ?as=user 로 역할 전환(api.ts 포워딩 활용), afterAll API 정리 - npm run test:e2e 스크립트, test-results/·playwright-report/ gitignore Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bce5016387
commit
f902fdb3ee
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,3 +8,6 @@ dist/
|
|||||||
build/
|
build/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
playwright/.cache/
|
||||||
|
|||||||
36
e2e/member-domains.spec.ts
Normal file
36
e2e/member-domains.spec.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
// 일반 구성원 관점의 핵심 도메인: 근무(휴가 신청 검증) + 인센티브 게이지.
|
||||||
|
// ?as=user 로 로드하면 앱 전체가 비관리자(김주임)로 동작.
|
||||||
|
|
||||||
|
test("근무 페이지 + 휴가 신청 모달 검증", async ({ page }) => {
|
||||||
|
await page.goto("/attendance?as=user");
|
||||||
|
await expect(page.getByRole("heading", { name: "근무" })).toBeVisible();
|
||||||
|
await expect(page.getByText("이번 달 출근일")).toBeVisible();
|
||||||
|
// 휴가/공가 탭 → 휴가 신청 모달
|
||||||
|
await page.getByRole("button", { name: "휴가/공가" }).click();
|
||||||
|
await page.getByRole("button", { name: "휴가 신청" }).click();
|
||||||
|
const submit = page.getByRole("button", { name: "신청", exact: true });
|
||||||
|
// 시작일 없으면 신청 비활성
|
||||||
|
await expect(submit).toBeDisabled();
|
||||||
|
await page.locator('.fixed input[type="date"]').first().fill("2026-09-10");
|
||||||
|
await expect(submit).toBeEnabled();
|
||||||
|
// 제출하지 않고 닫기(영속 데이터 오염 방지)
|
||||||
|
await page.getByRole("dialog").getByRole("button", { name: "취소" }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("인센티브 페이지 게이지 렌더", async ({ page }) => {
|
||||||
|
const errs: string[] = [];
|
||||||
|
page.on("pageerror", (e) => errs.push(e.message));
|
||||||
|
await page.goto("/incentive?as=user");
|
||||||
|
await expect(page.getByRole("heading", { name: "내 인센티브" })).toBeVisible();
|
||||||
|
// 게이지/할당량 섹션 렌더(연도 선택 존재)
|
||||||
|
await expect(page.locator("select").first()).toBeVisible();
|
||||||
|
expect(errs, errs.join("\n")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("대시보드 인센티브 게이지 + 내 이슈 보드", async ({ page }) => {
|
||||||
|
await page.goto("/?as=user");
|
||||||
|
await expect(page.getByText(/안녕하세요/)).toBeVisible();
|
||||||
|
await expect(page.getByText("내 이슈")).toBeVisible();
|
||||||
|
});
|
||||||
61
e2e/project-flow.spec.ts
Normal file
61
e2e/project-flow.spec.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
// 실제 프로젝트 상세 워크플로(개요/작업자/타임라인 칸반/메일 탭 + 작업 생성→상세 모달).
|
||||||
|
const API = "http://localhost:8380/api";
|
||||||
|
const stamp = Date.now().toString().slice(-6);
|
||||||
|
let projectId = "";
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
const r = await fetch(`${API}/projects`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name: `E2E흐름-${stamp}`, clientDomain: "meditech.com" }),
|
||||||
|
});
|
||||||
|
const p = await r.json();
|
||||||
|
projectId = p.id;
|
||||||
|
// 작업자 1명 추가(담당자 옵션)
|
||||||
|
await fetch(`${API}/projects/${projectId}/members`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ memberEmail: "park@special-partners.com", portion: 100, role: "작업자" }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
if (projectId) await fetch(`${API}/projects/${projectId}`, { method: "DELETE" }).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("프로젝트 상세 — 개요 렌더", async ({ page }) => {
|
||||||
|
await page.goto(`/projects/${projectId}`);
|
||||||
|
await expect(page.getByRole("heading", { name: new RegExp(`E2E흐름-${stamp}`) })).toBeVisible();
|
||||||
|
await expect(page.getByText("컨설팅 종류")).toBeVisible();
|
||||||
|
await expect(page.getByPlaceholder(/주의할 점/)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("탭 이동 — 작업자/메일", async ({ page }) => {
|
||||||
|
await page.goto(`/projects/${projectId}`);
|
||||||
|
await page.getByRole("button", { name: "작업자", exact: true }).click();
|
||||||
|
await expect(page.getByText(/기여도\(portion\)/)).toBeVisible();
|
||||||
|
await expect(page.getByText("park@special-partners.com")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "메일", exact: true }).click();
|
||||||
|
// 로컬은 Google Workspace 연동 미설정 → 안내 empty state가 정상
|
||||||
|
await expect(page.getByText("메일 연동이 설정되지 않았습니다")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("타임라인 — 작업 추가 → 상세 모달 → 칸반 카드", async ({ page }) => {
|
||||||
|
const errs: string[] = [];
|
||||||
|
page.on("pageerror", (e) => errs.push(e.message));
|
||||||
|
await page.goto(`/projects/${projectId}`);
|
||||||
|
await page.getByRole("button", { name: "타임라인", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: "작업 추가" }).click();
|
||||||
|
const title = `E2E작업-${stamp}`;
|
||||||
|
await page.getByPlaceholder("예: 기술문서 초안 작성").fill(title);
|
||||||
|
await page.getByRole("button", { name: "추가", exact: true }).click();
|
||||||
|
// 생성 후 상세 모달이 열림(댓글 입력창으로 확인)
|
||||||
|
await expect(page.getByPlaceholder("댓글 입력…")).toBeVisible();
|
||||||
|
// 모달 닫기 → 칸반 카드에 작업명 표시
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await expect(page.getByPlaceholder("댓글 입력…")).toBeHidden();
|
||||||
|
await expect(page.getByText(title).first()).toBeVisible();
|
||||||
|
expect(errs, errs.join("\n")).toHaveLength(0);
|
||||||
|
});
|
||||||
102
e2e/spin.spec.ts
Normal file
102
e2e/spin.spec.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { test, expect, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
// 로컬 DEV_AUTH: 기본 로드는 super-admin(관리자), '/?as=user' 로드는 비관리자(김주임).
|
||||||
|
const API = "http://localhost:8380/api";
|
||||||
|
const stamp = Date.now().toString().slice(-6);
|
||||||
|
|
||||||
|
// 콘솔/페이지 에러를 모아 테스트에서 검증(치명적 JS 에러 감지).
|
||||||
|
function trackErrors(page: Page): string[] {
|
||||||
|
const errs: string[] = [];
|
||||||
|
page.on("pageerror", (e) => errs.push("pageerror: " + e.message));
|
||||||
|
page.on("console", (m) => { if (m.type() === "error") errs.push("console: " + m.text()); });
|
||||||
|
return errs;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.afterAll(async ({ request }) => {
|
||||||
|
// E2E로 만든 프로젝트/일정 정리
|
||||||
|
const projs = await (await fetch(`${API}/projects`)).json().catch(() => []);
|
||||||
|
for (const p of Array.isArray(projs) ? projs : []) {
|
||||||
|
if (typeof p.name === "string" && p.name.includes("E2E")) {
|
||||||
|
await fetch(`${API}/projects/${p.id}`, { method: "DELETE" }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const evs = await (await fetch(`${API}/calendar/events`)).json().catch(() => []);
|
||||||
|
for (const e of Array.isArray(evs) ? evs : []) {
|
||||||
|
if (typeof e.title === "string" && e.title.includes("E2E")) {
|
||||||
|
await fetch(`${API}/calendar/events/${e.id}`, { method: "DELETE" }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("관리자(super-admin)", () => {
|
||||||
|
test("대시보드 + 사이드바 네비", async ({ page }) => {
|
||||||
|
const errs = trackErrors(page);
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page.getByText(/안녕하세요/)).toBeVisible();
|
||||||
|
await expect(page.getByText("내 이슈")).toBeVisible();
|
||||||
|
// 사이드바: 일반 + 관리자 메뉴
|
||||||
|
await expect(page.getByRole("link", { name: "프로젝트", exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "캘린더" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "프로젝트 관리" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "회계" })).toBeVisible();
|
||||||
|
expect(errs, errs.join("\n")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("쪽지함 명칭", async ({ page }) => {
|
||||||
|
await page.goto("/inbox");
|
||||||
|
await expect(page.getByRole("heading", { name: "쪽지함" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("캘린더 페이지 + 일정 생성", async ({ page }) => {
|
||||||
|
const errs = trackErrors(page);
|
||||||
|
await page.goto("/calendar");
|
||||||
|
await expect(page.getByRole("heading", { name: "캘린더" })).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "일정 추가" }).click();
|
||||||
|
const title = `E2E일정-${stamp}`;
|
||||||
|
await page.getByPlaceholder("예: 고객 미팅").fill(title);
|
||||||
|
// 분류/시작일은 기본값(기타/오늘) 사용
|
||||||
|
await page.getByRole("button", { name: "추가", exact: true }).click();
|
||||||
|
await expect(page.getByTitle(new RegExp(title))).toBeVisible();
|
||||||
|
expect(errs, errs.join("\n")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("프로젝트 생성(모달) → 목록 반영", async ({ page }) => {
|
||||||
|
await page.goto("/admin/projects");
|
||||||
|
await expect(page.getByRole("heading", { name: "프로젝트 관리" })).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "프로젝트 생성" }).click();
|
||||||
|
const name = `E2E프로젝트-${stamp}`;
|
||||||
|
await page.getByPlaceholder("예: CardioScan FDA 510(k)").fill(name);
|
||||||
|
// 첫 번째 select = 업체: 첫 실제 항목 선택(생성 버튼 활성 조건)
|
||||||
|
await page.locator(".fixed select").first().selectOption({ index: 1 });
|
||||||
|
await page.getByRole("button", { name: "생성", exact: true }).click();
|
||||||
|
await expect(page.getByRole("link", { name: new RegExp(name) })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("검증: 프로젝트명 없으면 생성 비활성", async ({ page }) => {
|
||||||
|
await page.goto("/admin/projects");
|
||||||
|
await page.getByRole("button", { name: "프로젝트 생성" }).click();
|
||||||
|
await expect(page.getByRole("button", { name: "생성", exact: true })).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("일반 구성원(?as=user)", () => {
|
||||||
|
test("관리자 메뉴 숨김", async ({ page }) => {
|
||||||
|
await page.goto("/?as=user");
|
||||||
|
await expect(page.getByText(/안녕하세요/)).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "캘린더" })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: "프로젝트 관리" })).toHaveCount(0);
|
||||||
|
await expect(page.getByRole("link", { name: "회계" })).toHaveCount(0);
|
||||||
|
await expect(page.getByRole("link", { name: "승인 관리" })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("관리자 라우트 접근 시 대시보드로 리다이렉트", async ({ page }) => {
|
||||||
|
await page.goto("/admin/accounting?as=user");
|
||||||
|
await expect(page).toHaveURL(/\/$|\/\?/); // '/' 로 리다이렉트
|
||||||
|
await expect(page.getByText(/안녕하세요/)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("내 프로필 접근", async ({ page }) => {
|
||||||
|
await page.goto("/profile?as=user");
|
||||||
|
await expect(page.getByRole("heading", { name: "내 프로필" })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
64
package-lock.json
generated
64
package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"recharts": "^2.12.7"
|
"recharts": "^2.12.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
@ -867,6 +868,22 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.3",
|
"version": "1.23.3",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
|
||||||
@ -2645,6 +2662,53 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc -b --noEmit"
|
"typecheck": "tsc -b --noEmit",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
@ -22,6 +23,7 @@
|
|||||||
"recharts": "^2.12.7"
|
"recharts": "^2.12.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
|||||||
20
playwright.config.ts
Normal file
20
playwright.config.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from "@playwright/test";
|
||||||
|
|
||||||
|
// 로컬 스택(docker: frontend 5380 + backend 8380, DEV_AUTH)에 대한 E2E.
|
||||||
|
// 스택은 이미 떠 있다고 가정(docker compose up -d).
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./e2e",
|
||||||
|
timeout: 30_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
retries: 0,
|
||||||
|
reporter: [["list"]],
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:5380",
|
||||||
|
headless: true,
|
||||||
|
viewport: { width: 1280, height: 900 },
|
||||||
|
actionTimeout: 10_000,
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user