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
207 changes: 207 additions & 0 deletions .github/scripts/playwright-comment.cjs
Original file line number Diff line number Diff line change
@@ -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 ?
`<details>
<summary><strong>✅ Passed Tests (${totalPassedCount})</strong></summary>

${passedTestsList}

</details>
` : '';

//prettier-ignore
const failedTestListSection = totalFailedCount > 0 ?
`<details open>
<summary><strong>❌ Failed Tests (${totalFailedCount})</strong></summary>

${failedTestList}

</details>
` : '';

//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);
};
44 changes: 44 additions & 0 deletions .github/scripts/playwright-initial-comment.cjs
Original file line number Diff line number Diff line change
@@ -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('✅ 새 코멘트 생성 완료');
}
};
74 changes: 74 additions & 0 deletions .github/workflows/playwright-pr.yml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 6 additions & 6 deletions e2e/tests/profile.test.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
// 존재하지 않는 프로필 페이지 방문
Expand Down
11 changes: 7 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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('')`. */
Expand Down Expand Up @@ -74,5 +74,8 @@ export default defineConfig({
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
env: {
NEXT_PUBLIC_MSW_ENABLED: 'true',
},
},
});
8 changes: 3 additions & 5 deletions src/app/(user)/profile/[userId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { redirect } from 'next/navigation';

import { dehydrate, HydrationBoundary } from '@tanstack/react-query';

import { API } from '@/api';
Expand All @@ -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();

Expand Down