test(e2e): Playwright E2E 도입 — 로그인→강좌→퀴즈 + 멘토/감사로그
실제 브라우저로 '다 조립된 앱'(프론트+백엔드+DB)을 태우는 통합 테스트 6개: - 인증 가드(비로그인→로그인), 로그인 실패 안내, 학생 로그인→대시보드 - 학습 센터→강좌 열기, 퀴즈 풀기→제출→서버 채점 결과 - 멘토 로그인→멘토 화면+개인정보 접근 기록(SEC-02 감사로그까지 검증) 운영 무오염 설계: - deploy/docker-compose.e2e.yml — 별도 프로젝트(mirime2e)·네트워크·휘발성 DB(tmpfs) - deploy/Caddyfile.e2e — 평문 HTTP로 SPA+/api 프록시(운영과 동일 구조) - deploy/e2e.sh — 스택 기동→테스트 학생 API 생성→퀴즈 slug 자동조회→ Playwright(브라우저 내장 이미지) 실행→down -v(정리) - 백엔드 힙 256MB 제한(작은 인스턴스에서 운영과 공존) EC2에서 6/6 통과 확인. 러너가 운영과 같은 인스턴스라 push마다가 아닌 온디맨드(릴리스 전)로 운영 — 전용 러너 확보 시 CI 게이트로 승격. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
db54b9bc19
commit
1661591d8b
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,6 +3,10 @@ backend/target/
|
|||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/coverage/
|
frontend/coverage/
|
||||||
|
# E2E(Playwright) 산출물
|
||||||
|
frontend/playwright-report/
|
||||||
|
frontend/test-results/
|
||||||
|
frontend/playwright/.cache/
|
||||||
|
|
||||||
# 비밀값 — 절대 커밋 금지
|
# 비밀값 — 절대 커밋 금지
|
||||||
deploy/.env
|
deploy/.env
|
||||||
|
|||||||
23
deploy/CI.md
23
deploy/CI.md
@ -113,9 +113,30 @@ CI가 통과해야 하는 관문이 아래처럼 늘었다. **로컬에서 먼
|
|||||||
학생 실습 흐름: `git switch -c feat/내작업` → 커밋 → push → Gitea에서 PR 생성 →
|
학생 실습 흐름: `git switch -c feat/내작업` → 커밋 → push → Gitea에서 PR 생성 →
|
||||||
CI 초록불 + 멘토 승인 → merge. (실무 표준 흐름을 그대로 경험)
|
CI 초록불 + 멘토 승인 → merge. (실무 표준 흐름을 그대로 경험)
|
||||||
|
|
||||||
|
## E2E 테스트 (`deploy/e2e.sh`)
|
||||||
|
|
||||||
|
단위/컴포넌트 테스트가 "부품"을 본다면, E2E는 **다 조립된 앱을 진짜 브라우저로** 눌러 본다 —
|
||||||
|
프론트+백엔드+DB가 함께 도는 통합 경로를 태워 "연결 지점의 회귀"를 잡는다(Playwright).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/e2e.sh # 격리 임시 스택 기동 → 테스트 → down -v(정리)
|
||||||
|
```
|
||||||
|
|
||||||
|
검사하는 여정(6): 인증 가드(비로그인→로그인) · 로그인 실패 안내 · 학생 로그인→대시보드 ·
|
||||||
|
학습 센터→강좌 · **퀴즈 풀기→제출→채점** · 멘토 로그인→화면+감사로그.
|
||||||
|
|
||||||
|
**설계 — 운영 무오염**: 별도 프로젝트(`mirime2e`)·네트워크·**휘발성 DB(tmpfs)** 로 완전히 격리된
|
||||||
|
임시 스택을 띄우고(호스트 포트 8899), 테스트 학생을 API로 만들고, 브라우저 내장 이미지
|
||||||
|
(`mcr.microsoft.com/playwright`)로 돌린 뒤 통째로 내린다. 운영 DB/컨테이너는 건드리지 않는다.
|
||||||
|
|
||||||
|
> **왜 push마다 CI로 안 돌리나?** 러너가 운영과 같은 작은 인스턴스(3.7GB)에 있어, 매 push마다
|
||||||
|
> 두 번째 풀스택+브라우저를 띄우면 운영 메모리를 압박할 수 있다. 그래서 E2E는 **릴리스 전/필요 시
|
||||||
|
> 온디맨드**로 돌린다(백엔드 힙도 256MB로 제한). 전용 러너가 생기면 그때 CI 게이트로 승격한다.
|
||||||
|
|
||||||
## 다음 개선 (학생 티켓)
|
## 다음 개선 (학생 티켓)
|
||||||
|
|
||||||
- Maven·npm 캐시로 CI 실행 시간 단축
|
- Maven·npm 캐시로 CI 실행 시간 단축
|
||||||
- 커버리지 바닥을 단계적으로 상향(30%→40%→…)
|
- 커버리지 바닥을 단계적으로 상향(30%→40%→…)
|
||||||
- 스모크 테스트를 배포 스크립트에 자동 편입(deploy 후 자동 실행·실패 시 알림)
|
- 스모크 테스트를 배포 스크립트에 자동 편입(deploy 후 자동 실행·실패 시 알림)
|
||||||
- E2E 테스트(Playwright: 로그인→강좌→퀴즈)
|
- React 컴포넌트 테스트(Testing Library) — 프론트 커버리지 확장
|
||||||
|
- 전용 러너 확보 시 E2E를 CI 게이트로 승격(현재는 온디맨드 `deploy/e2e.sh`)
|
||||||
|
|||||||
13
deploy/Caddyfile.e2e
Normal file
13
deploy/Caddyfile.e2e
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# E2E 전용 Caddy 설정 — 운영과 달리 HTTPS/도메인 없이 평문 HTTP로만 서빙한다.
|
||||||
|
# (E2E는 격리된 임시 스택이라 인증서가 필요 없다.) 역할은 운영과 동일:
|
||||||
|
# /api/* → 백엔드(backend:8080), 나머지 → SPA 정적 파일(+ 새로고침 대비 index.html 폴백).
|
||||||
|
:80 {
|
||||||
|
handle /api/* {
|
||||||
|
reverse_proxy backend:8080
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
root * /srv
|
||||||
|
try_files {path} /index.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
}
|
||||||
59
deploy/docker-compose.e2e.yml
Normal file
59
deploy/docker-compose.e2e.yml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# E2E 전용 임시 스택 — 운영과 완전히 격리(별도 프로젝트명 mirime2e, 별도 네트워크, 휘발성 DB).
|
||||||
|
# 운영 DB/컨테이너는 절대 건드리지 않는다. E2E가 끝나면 통째로 내려간다(down -v).
|
||||||
|
#
|
||||||
|
# 학습 포인트: E2E는 "진짜 같은 환경"이되 "버려도 되는 환경"이라야 한다.
|
||||||
|
# - Postgres 데이터는 tmpfs(메모리)에 둬 매 실행 깨끗하게 시작하고 흔적을 안 남긴다.
|
||||||
|
# - 백엔드 힙을 256MB로 제한해(작은 인스턴스에서 운영과 공존) 자원을 아낀다.
|
||||||
|
# - 멘토 초기 비밀번호를 알려진 값으로 고정해 E2E가 로그인할 수 있게 한다.
|
||||||
|
name: mirime2e
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: mirim
|
||||||
|
POSTGRES_USER: mirim
|
||||||
|
POSTGRES_PASSWORD: e2epass
|
||||||
|
tmpfs:
|
||||||
|
- /var/lib/postgresql/data # 휘발성 — 매 실행 새 DB, 디스크에 안 남음
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U mirim"]
|
||||||
|
interval: 4s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 15
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build: ../backend
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_NAME: mirim
|
||||||
|
DB_USER: mirim
|
||||||
|
DB_PASSWORD: e2epass
|
||||||
|
COOKIE_SECURE: "false" # 평문 HTTP라 secure 쿠키는 꺼야 로그인된다
|
||||||
|
LOG_LEVEL: INFO
|
||||||
|
MENTOR_INITIAL_PASSWORD: e2e-mentor-pass
|
||||||
|
JAVA_TOOL_OPTIONS: "-Xmx256m" # 작은 인스턴스에서 운영과 공존하도록 힙 제한
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# 프론트 빌드 결과물을 webroot 볼륨에 복사(운영과 동일한 방식)
|
||||||
|
frontend-build:
|
||||||
|
build: ../frontend
|
||||||
|
volumes:
|
||||||
|
- webroot:/out
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
ports:
|
||||||
|
- "8899:80" # 호스트 8899 → E2E 사이트(운영 80/443과 안 겹침)
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile.e2e:/etc/caddy/Caddyfile:ro
|
||||||
|
- webroot:/srv:ro
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
- frontend-build
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
webroot:
|
||||||
60
deploy/e2e.sh
Normal file
60
deploy/e2e.sh
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 이 파일이 하는 일: 격리된 임시 스택을 띄우고 Playwright E2E를 돌린 뒤 통째로 정리한다.
|
||||||
|
#
|
||||||
|
# 흐름: 스택 기동(postgres+backend+frontend+caddy) → 백엔드 준비 대기 → 테스트 학생 생성
|
||||||
|
# → 퀴즈 있는 코스 slug 조회 → Playwright(브라우저 내장 이미지) 실행 → down -v(정리).
|
||||||
|
#
|
||||||
|
# 사용법: bash deploy/e2e.sh
|
||||||
|
# 운영과 완전히 격리된다(별도 프로젝트·네트워크·휘발성 DB, 호스트 포트 8899). 운영 DB 무접촉.
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")" # deploy/
|
||||||
|
COMPOSE="docker compose -f docker-compose.e2e.yml"
|
||||||
|
NET="mirime2e_default"
|
||||||
|
BASE="http://localhost:8899"
|
||||||
|
|
||||||
|
cleanup() { echo "▶ E2E 스택 정리(down -v)..."; $COMPOSE down -v >/dev/null 2>&1 || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "▶ E2E 스택 빌드·기동..."
|
||||||
|
$COMPOSE up -d --build
|
||||||
|
|
||||||
|
echo "▶ 백엔드 준비 대기(최대 ~3분)..."
|
||||||
|
ready=0
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
code=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/auth/me" 2>/dev/null || true)
|
||||||
|
if [ "$code" = "401" ]; then ready=1; echo " 준비됨"; break; fi
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
if [ "$ready" != "1" ]; then
|
||||||
|
echo "🔴 백엔드가 뜨지 않았습니다. 최근 로그:"; $COMPOSE logs backend 2>&1 | tail -30; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "▶ 테스트 학생 생성(주소는 문자열 — Daum 팝업 우회)..."
|
||||||
|
sc=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/auth/signup" \
|
||||||
|
-H 'Content-Type: application/json' -d '{
|
||||||
|
"username":"e2estudent","password":"E2ePass123!","name":"이투이","track":"DEV",
|
||||||
|
"companyEmailLocal":"e2e.student","birthDate":"2007-03-15","phone":"010-1234-5678",
|
||||||
|
"address":"서울특별시 강남구 테헤란로 1","school":"미림마이스터고","major":"소프트웨어과",
|
||||||
|
"emergencyName":"이보호","emergencyRelation":"부","emergencyPhone":"010-9876-5432","privacyConsent":true
|
||||||
|
}')
|
||||||
|
echo " signup 응답: $sc (201=생성 / 409=이미 있음 — 둘 다 정상)"
|
||||||
|
|
||||||
|
echo "▶ 퀴즈가 있는 코스 slug 조회..."
|
||||||
|
SLUG=$($COMPOSE exec -T postgres psql -U mirim -d mirim -tAc \
|
||||||
|
"SELECT course_slug FROM quiz_question GROUP BY course_slug ORDER BY course_slug LIMIT 1" 2>/dev/null | tr -d '[:space:]')
|
||||||
|
echo " quiz slug: ${SLUG:-(없음 — 퀴즈 테스트는 skip)}"
|
||||||
|
|
||||||
|
echo "▶ Playwright 실행(브라우저 내장 이미지)..."
|
||||||
|
docker run --rm --network "$NET" \
|
||||||
|
-v "$PWD/../frontend:/work" -w /work \
|
||||||
|
-e E2E_BASE_URL=http://caddy \
|
||||||
|
-e E2E_STUDENT_USER=e2estudent -e E2E_STUDENT_PASS='E2ePass123!' \
|
||||||
|
-e E2E_MENTOR_USER=mentor1 -e E2E_MENTOR_PASS=e2e-mentor-pass \
|
||||||
|
-e E2E_QUIZ_SLUG="$SLUG" \
|
||||||
|
-e CI=true \
|
||||||
|
mcr.microsoft.com/playwright:v1.49.1-jammy \
|
||||||
|
sh -c "npm ci --no-audit --no-fund && npx playwright test"
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
echo "▶ Playwright 종료 코드: $rc"
|
||||||
|
exit $rc
|
||||||
27
frontend/e2e/auth.spec.js
Normal file
27
frontend/e2e/auth.spec.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, STUDENT } from './helpers.js'
|
||||||
|
|
||||||
|
// 인증 경계 — "로그인 전/후"가 올바로 갈리는지 실제 브라우저로 확인.
|
||||||
|
|
||||||
|
test('비로그인으로 보호 페이지 접근 → 로그인 화면으로 보낸다', async ({ page }) => {
|
||||||
|
await page.goto('/')
|
||||||
|
await expect(page).toHaveURL(/\/login/)
|
||||||
|
await expect(page.locator('#username')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('잘못된 비밀번호는 로그인 실패 안내를 보여준다', async ({ page }) => {
|
||||||
|
await page.goto('/login')
|
||||||
|
await page.locator('#username').fill(STUDENT.username)
|
||||||
|
await page.locator('#password').fill('wrong-password-xyz')
|
||||||
|
await page.getByRole('button', { name: '로그인' }).click()
|
||||||
|
await expect(page.getByText(/맞지 않아요|올바르지|너무 많/)).toBeVisible()
|
||||||
|
// 실패했으니 여전히 로그인 화면에 남아 있어야 한다.
|
||||||
|
await expect(page).toHaveURL(/\/login/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('학생 로그인 → 대시보드 진입', async ({ page }) => {
|
||||||
|
await login(page, STUDENT)
|
||||||
|
await expect(page).toHaveURL(/\/$/)
|
||||||
|
// 로그인 화면 요소(#username)는 더 이상 없어야 한다.
|
||||||
|
await expect(page.locator('#username')).toHaveCount(0)
|
||||||
|
})
|
||||||
23
frontend/e2e/helpers.js
Normal file
23
frontend/e2e/helpers.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
// 테스트 계정·퀴즈 slug는 deploy/e2e.sh 가 임시 스택을 띄우며 env로 주입한다.
|
||||||
|
// (기본값은 로컬에서 같은 값으로 스택을 띄웠을 때를 위한 폴백)
|
||||||
|
export const STUDENT = {
|
||||||
|
username: process.env.E2E_STUDENT_USER || 'e2estudent',
|
||||||
|
password: process.env.E2E_STUDENT_PASS || 'E2ePass123!',
|
||||||
|
}
|
||||||
|
export const MENTOR = {
|
||||||
|
username: process.env.E2E_MENTOR_USER || 'mentor1',
|
||||||
|
password: process.env.E2E_MENTOR_PASS || 'e2e-mentor-pass',
|
||||||
|
}
|
||||||
|
export const QUIZ_SLUG = process.env.E2E_QUIZ_SLUG || ''
|
||||||
|
|
||||||
|
/** 로그인 폼을 채워 제출하고, 대시보드(/)로 이동할 때까지 기다린다. */
|
||||||
|
export async function login(page, account) {
|
||||||
|
await page.goto('/login')
|
||||||
|
await page.locator('#username').fill(account.username)
|
||||||
|
await page.locator('#password').fill(account.password)
|
||||||
|
await page.getByRole('button', { name: '로그인' }).click()
|
||||||
|
await page.waitForURL((url) => url.pathname === '/', { timeout: 15_000 })
|
||||||
|
await expect(page.getByText('주차 진행 상황', { exact: false })).toBeVisible()
|
||||||
|
}
|
||||||
48
frontend/e2e/learn-quiz.spec.js
Normal file
48
frontend/e2e/learn-quiz.spec.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, STUDENT, QUIZ_SLUG } from './helpers.js'
|
||||||
|
|
||||||
|
// 핵심 사용자 여정: 로그인 → 학습 센터 → 강좌 → 퀴즈 채점.
|
||||||
|
// (프론트 SPA 라우팅 + 백엔드 API + DB가 함께 도는 통합 경로를 실제로 태운다.)
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await login(page, STUDENT)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('학습 센터 → 카테고리 → 강좌 열기', async ({ page }) => {
|
||||||
|
await page.goto('/learn')
|
||||||
|
await expect(page.getByRole('heading', { name: '어썸데브 학습 센터' })).toBeVisible()
|
||||||
|
|
||||||
|
// 강좌 카드(링크)가 보이고, 클릭하면 강좌 페이지로 이동한다.
|
||||||
|
const courseLink = page.locator('a[href^="/learn/"]').first()
|
||||||
|
await expect(courseLink).toBeVisible()
|
||||||
|
await courseLink.click()
|
||||||
|
await expect(page).toHaveURL(/\/learn\/[^/]+$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('퀴즈 풀기 → 제출 → 채점 결과 표시', async ({ page }) => {
|
||||||
|
test.skip(!QUIZ_SLUG, '퀴즈가 있는 코스 slug가 주입되지 않았음(E2E_QUIZ_SLUG)')
|
||||||
|
|
||||||
|
// "안 푼 문제가 있어요" 확인창이 뜨면 자동 수락(모두 풀 것이므로 보통 안 뜬다).
|
||||||
|
page.on('dialog', (dialog) => dialog.accept())
|
||||||
|
|
||||||
|
await page.goto(`/learn/${QUIZ_SLUG}/quiz`)
|
||||||
|
|
||||||
|
// 제출 버튼이 보일 때까지 = 문항 로드 완료.
|
||||||
|
const submit = page.getByRole('button', { name: '제출하고 채점받기' })
|
||||||
|
await expect(submit).toBeVisible()
|
||||||
|
|
||||||
|
// 각 문항 카드에서 첫 번째 보기를 고른다.
|
||||||
|
// 보기는 라디오를 감싼 <label>이므로, 라벨을 클릭하면 해당 보기가 선택된다.
|
||||||
|
const questionCards = page.locator('.card').filter({ has: page.locator('input[type=radio]') })
|
||||||
|
const count = await questionCards.count()
|
||||||
|
expect(count).toBeGreaterThan(0)
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
await questionCards.nth(i).locator('label').first().click()
|
||||||
|
}
|
||||||
|
|
||||||
|
await submit.click()
|
||||||
|
|
||||||
|
// 서버 채점 결과가 표시된다(점수).
|
||||||
|
await expect(page.getByText(/채점 완료/)).toBeVisible()
|
||||||
|
await expect(page.locator('.stat-num')).toContainText('/')
|
||||||
|
})
|
||||||
20
frontend/e2e/mentor.spec.js
Normal file
20
frontend/e2e/mentor.spec.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
import { login, MENTOR } from './helpers.js'
|
||||||
|
|
||||||
|
// 멘토 여정: 로그인 → 멘토 화면 → 인적사항 명단 + 개인정보 접근 기록(감사로그).
|
||||||
|
// 명단을 조회하는 순간 백엔드가 감사기록(VIEW_STUDENT_ROSTER)을 남기고, 화면에 보인다 —
|
||||||
|
// SEC-02 감사로그가 실제로 "쓰이고 보이는지"까지 한 번에 검증한다.
|
||||||
|
|
||||||
|
test('멘토 로그인 → 멘토 화면 + 감사로그 표시', async ({ page }) => {
|
||||||
|
await login(page, MENTOR)
|
||||||
|
|
||||||
|
await page.goto('/mentor')
|
||||||
|
await expect(page.getByRole('heading', { name: '멘토 리뷰' })).toBeVisible()
|
||||||
|
|
||||||
|
// 수습생 명단 섹션
|
||||||
|
await expect(page.getByRole('heading', { name: '수습생 명단' })).toBeVisible()
|
||||||
|
|
||||||
|
// 개인정보 접근 기록(감사로그) 섹션 + 방금 명단 조회가 기록으로 남았는지
|
||||||
|
await expect(page.getByRole('heading', { name: '개인정보 접근 기록' })).toBeVisible()
|
||||||
|
await expect(page.getByText('명단 조회', { exact: false }).first()).toBeVisible()
|
||||||
|
})
|
||||||
@ -9,8 +9,8 @@ import globals from 'globals'
|
|||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
// 검사에서 제외할 폴더 (빌드 산출물·커버리지·의존성)
|
// 검사에서 제외할 폴더 (빌드 산출물·커버리지·의존성·E2E 리포트)
|
||||||
{ ignores: ['dist', 'coverage', 'node_modules'] },
|
{ ignores: ['dist', 'coverage', 'node_modules', 'playwright-report', 'test-results'] },
|
||||||
|
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx}'],
|
files: ['**/*.{js,jsx}'],
|
||||||
|
|||||||
64
frontend/package-lock.json
generated
64
frontend/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@playwright/test": "1.49.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/coverage-v8": "^4.1.10",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
@ -1032,6 +1033,22 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.49.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
|
||||||
|
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.49.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
@ -3171,6 +3188,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.49.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
|
||||||
|
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.49.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.49.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
|
||||||
|
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
|
||||||
|
"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.19",
|
"version": "8.5.19",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
@ -20,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@playwright/test": "1.49.1",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@vitest/coverage-v8": "^4.1.10",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
|
|||||||
29
frontend/playwright.config.js
Normal file
29
frontend/playwright.config.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
// 이 파일이 하는 일: E2E(End-to-End) 테스트 설정.
|
||||||
|
//
|
||||||
|
// 학습 포인트 — E2E는 무엇을 검증하나?
|
||||||
|
// 단위/컴포넌트 테스트는 "부품 하나"를 본다. E2E는 "다 조립된 앱"을 진짜 브라우저로
|
||||||
|
// 사용자처럼 눌러 본다 — 프론트+백엔드+DB가 함께 도는 통합 경로(로그인→강좌→퀴즈)를
|
||||||
|
// 확인해, 부품별 테스트가 못 잡는 "연결 지점의 회귀"를 잡는다.
|
||||||
|
//
|
||||||
|
// 스택(프론트+백엔드+DB)은 deploy/e2e.sh 가 격리된 임시 환경으로 띄우고(운영 무오염),
|
||||||
|
// 이 설정은 그 주소(E2E_BASE_URL)를 브라우저로 연다. 계정/퀴즈 slug도 env로 주입받는다.
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
timeout: 30_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
// 하나의 임시 DB를 공유하므로 순차 실행(계정/세션이 서로 안 얽히게).
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
// 네트워크 타이밍으로 한 번 실패할 수 있으니 1회 재시도(진짜 회귀는 재시도해도 실패).
|
||||||
|
retries: 1,
|
||||||
|
reporter: [['list'], ['html', { open: 'never' }]],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.E2E_BASE_URL || 'http://localhost:8899',
|
||||||
|
trace: 'on-first-retry', // 재시도 시 추적 파일 남겨 실패 원인 분석
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
actionTimeout: 10_000,
|
||||||
|
},
|
||||||
|
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user