From 517dd1cef45be15c859462de696730717dbffbd1 Mon Sep 17 00:00:00 2001 From: hyemin Date: Tue, 16 Jun 2026 13:18:44 +0900 Subject: [PATCH 1/2] =?UTF-8?q?test=20(#12)=20::=20Playwright=20E2E=20base?= =?UTF-8?q?line=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constraint: Playwright route smoke tests must run against production-equivalent Next start without adding product UI scope.\nRejected: Browser-based smoke tests | Route status and SSR content checks do not require browser binaries yet.\nConfidence: high\nScope-risk: narrow\nDirective: Keep this PR limited to test infrastructure and route smoke coverage; defer auth UI, dropdown a11y, and route policy abstraction.\nTested: pnpm lint; pnpm build; pnpm test:e2e; git diff --check\nNot-tested: GitHub Actions execution before PR checks run remotely --- .github/workflows/ci.yml | 39 ++++++++++++++++++++++++++++++ .gitignore | 5 ++++ package.json | 4 ++- playwright.config.ts | 23 ++++++++++++++++++ pnpm-lock.yaml | 43 +++++++++++++++++++++++++++++++-- tests/e2e/public-routes.spec.ts | 38 +++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 playwright.config.ts create mode 100644 tests/e2e/public-routes.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5f5ba48 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +jobs: + verify: + name: Lint, build, and E2E smoke + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.20.2 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: E2E smoke + run: pnpm test:e2e diff --git a/.gitignore b/.gitignore index 7d76170..ef313fc 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,8 @@ out .codex/skills/.system/** !.codex/prompts/ !.codex/prompts/** + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ diff --git a/package.json b/package.json index 38c1417..6c20818 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "next dev", "build": "next build", "lint": "eslint .", - "start": "next start" + "start": "next start", + "test:e2e": "playwright test" }, "dependencies": { "next": "^16.2.7", @@ -16,6 +17,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@playwright/test": "^1.60.0", "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..97fc1d8 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from '@playwright/test' + +const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 3100) +const baseURL = `http://127.0.0.1:${PORT}` + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30_000, + expect: { + timeout: 5_000, + }, + fullyParallel: true, + reporter: process.env.CI ? 'github' : 'list', + use: { + baseURL, + }, + webServer: { + command: `pnpm build && pnpm exec next start -H 127.0.0.1 -p ${PORT}`, + url: baseURL, + reuseExistingServer: false, + timeout: 120_000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30c3aeb..79a6288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: next: specifier: ^16.2.7 - version: 16.2.7(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 16.2.7(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: ^19.2.6 version: 19.2.7 @@ -21,6 +21,9 @@ importers: '@eslint/js': specifier: ^9.39.4 version: 9.39.4 + '@playwright/test': + specifier: ^1.60.0 + version: 1.60.0 '@types/node': specifier: ^24.12.3 version: 24.12.4 @@ -434,6 +437,11 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} + hasBin: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1060,6 +1068,11 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -1490,6 +1503,16 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2137,6 +2160,10 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@playwright/test@1.60.0': + dependencies: + playwright: 1.60.0 + '@rtsao/scc@1.1.0': {} '@swc/helpers@0.5.15': @@ -2913,6 +2940,9 @@ snapshots: dependencies: is-callable: 1.2.7 + fsevents@2.3.2: + optional: true + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -3232,7 +3262,7 @@ snapshots: natural-compare@1.4.0: {} - next@16.2.7(@babel/core@7.29.7)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.2.7(@babel/core@7.29.7)(@playwright/test@1.60.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@next/env': 16.2.7 '@swc/helpers': 0.5.15 @@ -3251,6 +3281,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.7 '@next/swc-win32-arm64-msvc': 16.2.7 '@next/swc-win32-x64-msvc': 16.2.7 + '@playwright/test': 1.60.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -3346,6 +3377,14 @@ snapshots: picomatch@4.0.4: {} + playwright-core@1.60.0: {} + + playwright@1.60.0: + dependencies: + playwright-core: 1.60.0 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss@8.4.31: diff --git a/tests/e2e/public-routes.spec.ts b/tests/e2e/public-routes.spec.ts new file mode 100644 index 0000000..d899494 --- /dev/null +++ b/tests/e2e/public-routes.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@playwright/test' + +test.describe('public route smoke', () => { + test('renders the public home route', async ({ request }) => { + const response = await request.get('/') + + expect(response.status()).toBe(200) + await expect(response).toBeOK() + const body = await response.text() + + expect(body).toContain('대덕소프트마이스터고 학생을 위한 포트폴리오 플랫폼') + expect(body).toContain('공개 포트폴리오 예시') + }) + + test('renders a Korean public portfolio slug', async ({ request }) => { + const response = await request.get('/오혜민') + + expect(response.status()).toBe(200) + await expect(response).toBeOK() + const body = await response.text() + const normalizedBody = body.replaceAll(//g, '') + + expect(normalizedBody).toContain('오혜민') + expect(normalizedBody).toContain('/오혜민') + }) + + test('does not treat the reserved dev slug as a public portfolio', async ({ request }) => { + const response = await request.get('/dev') + + expect(response.status()).toBe(404) + }) + + test('does not expose the component preview in production', async ({ request }) => { + const response = await request.get('/dev/component-preview') + + expect(response.status()).toBe(404) + }) +}) From abeb200c1e81c18aaef72d9533e50daccfdd649e Mon Sep 17 00:00:00 2001 From: hyemin Date: Wed, 17 Jun 2026 14:26:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ci=20(#12)=20::=20CI=20Node=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=EA=B3=BC=20=EA=B6=8C=ED=95=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Constraint: PR review flagged exact Node version stability and missing minimal workflow permissions.\nRejected: Keeping exact 20.20.2 in CI | setup-node should track valid Node 20 releases reliably.\nConfidence: high\nScope-risk: narrow\nDirective: Keep Playwright webServer build ownership single-sourced in CI; do not expand this PR into product UI work.\nTested: pnpm lint; pnpm build; pnpm test:e2e; git diff --check\nNot-tested: None --- .github/workflows/ci.yml | 5 ++++- playwright.config.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f5ba48..6bbf838 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: branches: - main +permissions: + contents: read + jobs: verify: name: Lint, build, and E2E smoke @@ -23,7 +26,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20.20.2 + node-version: 20.x cache: pnpm - name: Install dependencies diff --git a/playwright.config.ts b/playwright.config.ts index 97fc1d8..30223cf 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from '@playwright/test' const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 3100) const baseURL = `http://127.0.0.1:${PORT}` +const startCommand = `pnpm exec next start -H 127.0.0.1 -p ${PORT}` export default defineConfig({ testDir: './tests/e2e', @@ -15,7 +16,7 @@ export default defineConfig({ baseURL, }, webServer: { - command: `pnpm build && pnpm exec next start -H 127.0.0.1 -p ${PORT}`, + command: process.env.CI ? startCommand : `pnpm build && ${startCommand}`, url: baseURL, reuseExistingServer: false, timeout: 120_000,