Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: E2E (Playwright)

on:
push:
branches: [main]
pull_request:

jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm exec playwright install --with-deps chromium

- name: Create .env.e2e from secrets
run: |
{
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.E2E_SUPABASE_URL }}"
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.E2E_SUPABASE_ANON_KEY }}"
echo "NEXT_PUBLIC_SITE_URL=${{ secrets.E2E_SITE_URL }}"
} > .env.e2e

- run: pnpm test:e2e

- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ out
*.sw?

.env
.env.e2e
next-env.d.ts
tsconfig.tsbuildinfo

Expand All @@ -34,3 +35,9 @@ supabase/*

# Local quiz content data
src/features/study-quiz/mock/miniQuizData.ts

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
38 changes: 38 additions & 0 deletions e2e/daily-quiz.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect, test } from "@playwright/test";

// 흐름 3: (게스트) 데일리 퀴즈 도전 → 완주 후 로그인 유도
// 게스트 결과는 랭킹에 기록되지 않고 소셜 로그인을 유도한다(의도된 동작).
// 실제 문제라 정답을 모르지만, 완주 흐름만 검증하므로 아무 답이나 제출한다.
test("게스트가 데일리 퀴즈를 완주하면 로그인 유도 안내가 뜬다", async ({ page }) => {
await page.goto("/auth/guest");
await expect(page).toHaveURL(/\/lobby$/);

await page.goto("/challenge");
await page.getByRole("button", { name: "도전 시작하기" }).click();
await page.getByRole("link", { name: "시작하기" }).click();
await expect(page).toHaveURL(/\/challenge\/play$/);

for (let questionIndex = 0; questionIndex < 5; questionIndex += 1) {
const commandInput = page.getByRole("textbox");

if (await commandInput.count()) {
await commandInput.fill("git status");
} else {
await page.locator("button[data-status]").first().click();
}

await page.getByRole("button", { name: "제출하기" }).click();
await page
.getByRole("button", {
name: questionIndex === 4 ? "결과 보기" : "다음 문제 풀기",
})
.click();
}

await expect(
page.getByRole("heading", { name: "오늘의 도전을 마쳤어요!" }),
).toBeVisible();
await expect(
page.getByText("지금 결과는 저장되지 않았어요. 로그인하면 랭킹에 도전할 수 있어요."),
).toBeVisible();
});
19 changes: 19 additions & 0 deletions e2e/lobby-entry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, test } from "@playwright/test";

// 흐름 1: (게스트) 로비 진입
test("게스트가 홈에서 로비까지 진입한다", async ({ page }) => {
await page.goto("/");

await page.getByRole("button", { name: "시작하기!" }).click();
await expect(
page.getByRole("heading", { name: "꼬깃 시작하기" }),
).toBeVisible();

await page.getByRole("button", { name: "게스트로 둘러보기" }).click();
await expect(page).toHaveURL(/\/lobby$/);

await expect(page.getByRole("heading", { name: "이어하기" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "데일리 퀘스트" }),
).toBeVisible();
});
36 changes: 36 additions & 0 deletions e2e/mini-quiz-clear.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, test } from "@playwright/test";

// 흐름 2: (게스트) 미니 퀴즈 스테이지 클리어 → 별 반영
// 게스트 문제는 결정론적: 객관식은 첫 보기가 정답, 명령어는 정답이 placeholder("예: <명령어>")에 있다.
test("게스트가 미니 퀴즈 스테이지를 클리어하고 별을 받는다", async ({ page }) => {
await page.goto("/auth/guest");
await expect(page).toHaveURL(/\/lobby$/);
await page.goto("/study");

await page.locator('button[data-status="available"]').first().click();
await page.getByRole("link", { name: "시작하기" }).click();
await expect(page).toHaveURL(/\/study\/[^/]+\/\d+$/);

for (let questionIndex = 0; questionIndex < 5; questionIndex += 1) {
const commandInput = page.getByRole("textbox");

if (await commandInput.count()) {
const placeholder = await commandInput.getAttribute("placeholder");
await commandInput.fill((placeholder ?? "").replace(/^예:\s*/, ""));
} else {
await page.locator("button[data-status]").first().click();
}

await page.getByRole("button", { name: "제출하기" }).click();
await page
.getByRole("button", {
name: questionIndex === 4 ? "결과 보기" : "다음 문제 풀기",
})
.click();
}

await expect(
page.getByRole("heading", { name: "스테이지를 클리어했어요!" }),
).toBeVisible();
await expect(page.getByLabel("별 3개")).toBeVisible();
});
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"lint": "eslint",
"typecheck": "tsc --noEmit",
"test": "vitest",
"test:run": "vitest run"
"test:run": "vitest run",
"test:e2e": "playwright test"
},
"dependencies": {
"@supabase/ssr": "^0.10.3",
Expand All @@ -31,6 +32,7 @@
"zustand": "^5.0.14"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
Expand Down
43 changes: 43 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { readFileSync } from "node:fs";

import { defineConfig, devices } from "@playwright/test";

// E2E 전용 환경변수(.env.e2e)를 읽어 dev 서버로 넘긴다.
// 테스트 전용 Supabase를 가리켜 운영 DB 오염을 막는다.
const loadE2eEnv = (): Record<string, string> => {
try {
return Object.fromEntries(
readFileSync(".env.e2e", "utf8")
.split("\n")
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
.map((line) => {
const index = line.indexOf("=");
return [line.slice(0, index), line.slice(index + 1)];
}),
);
} catch {
return {};
}
};

export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI ? [["list"], ["html", { open: "never" }]] : "list",
use: {
// route-151 등이 :3000을 쓰므로 E2E는 전용 포트 :3100에 띄운다.
baseURL: "http://localhost:3100",
trace: "on-first-retry",
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "pnpm dev --port 3100",
url: "http://localhost:3100",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
env: loadE2eEnv(),
},
});
55 changes: 47 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading