diff --git a/.github/scripts/playwright-comment.cjs b/.github/scripts/playwright-comment.cjs new file mode 100644 index 00000000..8aeba9e2 --- /dev/null +++ b/.github/scripts/playwright-comment.cjs @@ -0,0 +1,207 @@ +module.exports = async ({ github, context }) => { + const fs = require('fs'); + + const buildUrl = process.env.BUILD_URL || ''; + const now = new Date().toISOString().replace('T', ' ').split('.')[0]; + + const title = '## 🎭 Playwright Report'; + const descriptionSuccess = ` +✨ **E2E Testκ°€ μ„±κ³΅μ μœΌλ‘œ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.** + +Test μš”μ•½ λ‚΄μš©μ„ ν™•μΈν•΄μ£Όμ„Έμš”. +`; + const descriptionSkipped = ` +✨ **E2E Test 파일이 κ°μ§€λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.** + +이 PRμ—λŠ” Test File이 μ—†μ–΄μ„œ Reportλ₯Ό μƒμ„±ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. +`; + const descriptionFailed = ` +❌ **E2E Test에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.** + +Test Logλ₯Ό ν™•μΈν•˜μ‹œκ³  λ‘œμ§μ„ μˆ˜μ •ν•΄μ£Όμ„Έμš”. +`; + + const statusTableSuccess = ` +| Status | Build Log | Updated (UTC) | +|--------|-----------|---------------| +| βœ… Ready | [View Build](${buildUrl}) | ${now} | +`; + + const statusTableSkipped = ` +| Status | Build Log | Updated (UTC) | +|--------|-----------|---------------| +| ⏭️ Skipped | - | ${now} | +`; + + const statusTableFailed = ` +| Status | Build Log | Updated (UTC) | +|--------|-----------|---------------| +| ❌ Failed | [View Logs](${buildUrl}) | ${now} | +`; + + const results = JSON.parse(fs.readFileSync('test-results/results.json', 'utf8')); + + const passed = results.stats.expected; + const failed = results.stats.unexpected; + const duration = (results.stats.duration / 1000).toFixed(1); + + // μ„±κ³΅ν•œ ν…ŒμŠ€νŠΈ λͺ©λ‘ + const successTestsByFile = results.suites.reduce((acc, suite) => { + const fileName = suite.file; + const passedSpecs = suite.specs.filter((spec) => spec.ok); + + if (passedSpecs.length > 0) { + acc[fileName] = passedSpecs.map((spec) => ({ + project: spec.tests[0].projectName, + title: spec.title, + })); + } + + return acc; + }, {}); + + const totalPassedCount = Object.values(successTestsByFile).flat().length; + + const passedTestsList = Object.entries(successTestsByFile) + .map(([file, tests]) => { + const testList = tests.map((test) => ` - [${test.project}] ${test.title}`).join('\n'); + return `- **${file}** (${tests.length})\n${testList}`; + }) + .join('\n'); + + // μ‹€νŒ¨ν•œ ν…ŒμŠ€νŠΈ λͺ©λ‘ + const failedTestsByFile = results.suites.reduce((acc, suite) => { + const fileName = suite.file; + const failedSpecs = suite.specs.filter((spec) => !spec.ok); + + if (failedSpecs.length > 0) { + acc[fileName] = failedSpecs.map((spec) => ({ + project: spec.tests[0].projectName, + title: spec.title, + error: spec.tests[0].results[0].errors[0]?.message || 'Unknown error', + })); + } + + return acc; + }, {}); + + const totalFailedCount = Object.values(failedTestsByFile).flat().length; + + // μ‹€νŒ¨ν•œ ν…ŒμŠ€νŠΈ λͺ©λ‘ + const failedTestList = Object.entries(failedTestsByFile) + .map(([file, tests]) => { + const testList = tests + .map( + (test) => + ` - [${test.project}] ${test.title}\n \`\`\`\n ${test.error.split('\n')[0]}\n \`\`\``, + ) + .join('\n'); + return `- **${file}** (${tests.length})\n${testList}`; + }) + .join('\n'); + + let resultType; + + if (passed + failed === 0) resultType = 'skipped'; + else if (failed > 0) resultType = 'failed'; + else resultType = 'success'; + + let nextDescription; + let nextStatusTable; + switch (resultType) { + case 'success': + nextDescription = descriptionSuccess; + nextStatusTable = statusTableSuccess; + break; + case 'skipped': + nextDescription = descriptionSkipped; + nextStatusTable = statusTableSkipped; + break; + case 'failed': + nextDescription = descriptionFailed; + nextStatusTable = statusTableFailed; + break; + } + + const testSummary = ` +### πŸ“Š Test Summary +- βœ… Passed: ${totalPassedCount} +- ❌ Failed: ${totalFailedCount} +- ⏱️ Duration: ${duration}s +`; + + //prettier-ignore + const successTestListSection = totalPassedCount > 0 ? +`
+βœ… Passed Tests (${totalPassedCount}) + +${passedTestsList} + +
+` : ''; + + //prettier-ignore + const failedTestListSection = totalFailedCount > 0 ? +`
+❌ Failed Tests (${totalFailedCount}) + +${failedTestList} + +
+` : ''; + + //prettier-ignore + const testResults = totalPassedCount + totalFailedCount > 0 ? ` +### πŸ“œ Test Details +${successTestListSection} + +${failedTestListSection} +` : ''; + + const comment = ` +${title} + +${nextDescription} + +${nextStatusTable} + +${testSummary} + +${testResults} +`; + + /** + * PR에 μ½”λ©˜νŠΈλ₯Ό μž‘μ„±ν•˜κ±°λ‚˜ μ—…λ°μ΄νŠΈν•˜λŠ” ν•¨μˆ˜ + */ + async function postOrUpdateComment(commentBody) { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find( + (comment) => comment.user.type === 'Bot' && comment.body.includes(title), + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody, + }); + console.log('βœ… Playwright μ½”λ©˜νŠΈκ°€ μ—…λ°μ΄νŠΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + } else { + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: commentBody, + }); + console.log('βœ… Playwright μ½”λ©˜νŠΈκ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + } + } + + await postOrUpdateComment(comment); +}; diff --git a/.github/scripts/playwright-initial-comment.cjs b/.github/scripts/playwright-initial-comment.cjs new file mode 100644 index 00000000..23afdec0 --- /dev/null +++ b/.github/scripts/playwright-initial-comment.cjs @@ -0,0 +1,44 @@ +module.exports = async ({ github, context }) => { + const buildLogUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`; + const now = new Date().toISOString().replace('T', ' ').split('.')[0]; + + const commentBody = `## 🎭 Playwright Report + +πŸ”„ **Playwright Reportλ₯Ό 생성 μ€‘μž…λ‹ˆλ‹€.** + +μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš”... + +| Status | Build Log | Updated (UTC) | +|--------|-----------|---------------| +| πŸ”„ Testing... | [View Logs](${buildLogUrl}) | ${now} |`; + + // κΈ°μ‘΄ μ½”λ©˜νŠΈ μ°ΎκΈ° + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.find( + (comment) => comment.user.type === 'Bot' && comment.body.includes('## 🎭 Playwright Report'), + ); + + // κΈ°μ‘΄ μ½”λ©˜νŠΈ 있으면 μˆ˜μ •, μ—†μœΌλ©΄ 생성 + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody, + }); + console.log('βœ… κΈ°μ‘΄ μ½”λ©˜νŠΈ μ—…λ°μ΄νŠΈ μ™„λ£Œ'); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: commentBody, + }); + console.log('βœ… μƒˆ μ½”λ©˜νŠΈ 생성 μ™„λ£Œ'); + } +}; diff --git a/.github/workflows/playwright-pr.yml b/.github/workflows/playwright-pr.yml new file mode 100644 index 00000000..59c632e4 --- /dev/null +++ b/.github/workflows/playwright-pr.yml @@ -0,0 +1,74 @@ +name: PR Playwright Report + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + coverage: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Create initial Report + if: always() + uses: actions/github-script@v7 + with: + script: | + const script = require('./.github/scripts/playwright-initial-comment.cjs'); + await script({ github, context }); + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm exec playwright install --with-deps + + - name: Check if tests exist + id: check-tests + run: | + if [ -d "e2e/tests" ] && [ "$(ls -A e2e/tests)" ]; then + echo "has_tests=true" >> $GITHUB_OUTPUT + else + echo "has_tests=false" >> $GITHUB_OUTPUT + fi + + - name: Run Playwright tests + id: test + if: steps.check-tests.outputs.has_tests == 'true' + run: pnpm test:playwright + continue-on-error: true + env: + NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL }} + + - name: Create Playwright report comment + if: always() + uses: actions/github-script@v7 + env: + TEST_OUTCOME: ${{ steps.test.outcome }} + BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + script: | + const script = require('./.github/scripts/playwright-comment.cjs'); + await script({ github, context }); + + - name: Fail if tests exist but failed + if: steps.check-tests.outputs.has_tests == 'true' && steps.test.outcome == 'failure' + run: | + echo "::error::Playwright tests failed! Please fix the failing tests." + exit 1 diff --git a/e2e/tests/profile.test.ts b/e2e/tests/profile.test.ts index 1000f4a9..e7d157ed 100644 --- a/e2e/tests/profile.test.ts +++ b/e2e/tests/profile.test.ts @@ -1,12 +1,12 @@ import { expect, test } from '@playwright/test'; -test('λ‚˜μ˜ ν”„λ‘œν•„ νŽ˜μ΄μ§€λ‘œ 접속 μ‹œ λ§ˆμ΄νŽ˜μ΄μ§€λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ λ˜λŠ” μ§€ ν…ŒμŠ€νŠΈ', async ({ page }) => { - // λ‚˜μ˜ ν”„λ‘œν•„ νŽ˜μ΄μ§€ λ°©λ¬Έ - await page.goto('/profile/1'); +// test('λ‚˜μ˜ ν”„λ‘œν•„ νŽ˜μ΄μ§€λ‘œ 접속 μ‹œ λ§ˆμ΄νŽ˜μ΄μ§€λ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ λ˜λŠ” μ§€ ν…ŒμŠ€νŠΈ', async ({ page }) => { +// // λ‚˜μ˜ ν”„λ‘œν•„ νŽ˜μ΄μ§€ λ°©λ¬Έ +// await page.goto('/profile/1'); - // redirect λŒ€κΈ° - await expect(page).toHaveURL('/mypage'); -}); +// // redirect λŒ€κΈ° +// await expect(page).toHaveURL('/mypage'); +// }); test('μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν”„λ‘œν•„ νŽ˜μ΄μ§€λ‘œ 접속 μ‹œ 404 λ¦¬λ‹€μ΄λ ‰νŠΈ λ˜λŠ” μ§€ ν…ŒμŠ€νŠΈ', async ({ page }) => { // μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν”„λ‘œν•„ νŽ˜μ΄μ§€ λ°©λ¬Έ diff --git a/playwright.config.ts b/playwright.config.ts index 9f999ce3..31d56b14 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,9 +3,9 @@ import { defineConfig, devices } from '@playwright/test'; * Read environment variables from file. * https://github.com/motdotla/dotenv */ -import dotenv from 'dotenv'; -import path from 'path'; -dotenv.config({ path: path.resolve(__dirname, '.env.test') }); +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env.test') }); /** * See https://playwright.dev/docs/test-configuration. @@ -21,7 +21,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: [['html'], ['json', { outputFile: 'test-results/results.json' }]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('')`. */ @@ -74,5 +74,8 @@ export default defineConfig({ command: 'pnpm dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, + env: { + NEXT_PUBLIC_MSW_ENABLED: 'true', + }, }, }); diff --git a/src/app/(user)/profile/[userId]/layout.tsx b/src/app/(user)/profile/[userId]/layout.tsx index 362caf09..a358c8f0 100644 --- a/src/app/(user)/profile/[userId]/layout.tsx +++ b/src/app/(user)/profile/[userId]/layout.tsx @@ -1,5 +1,3 @@ -import { redirect } from 'next/navigation'; - import { dehydrate, HydrationBoundary } from '@tanstack/react-query'; import { API } from '@/api'; @@ -16,9 +14,9 @@ const ProfileLayout = async ({ children, params }: Props) => { const userId = Number(id); // 본인 id와 같은지 확인 ν›„ κ°™μœΌλ©΄ mypage둜 λ¦¬λ‹€μ΄λ ‰νŠΈ - if (userId === 1) { - redirect('/mypage'); - } + // if (userId === 1) { + // redirect('/mypage'); + // } const queryClient = getQueryClient();