diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9094a3b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +# PR과 main 푸시에서 lint·타입체크·단위·E2E를 검증한다. +on: + push: + branches: [main] + pull_request: + branches: [main] + +# 같은 브랜치/PR의 진행 중 실행은 취소해 러너 사용량을 줄인다. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: lint · type-check · unit · e2e + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + # pnpm을 먼저 설치해야 setup-node의 pnpm 캐시가 동작한다. + # 버전은 루트 package.json의 packageManager(pnpm@9.0.0) 필드를 따른다. + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm --filter blog lint + + - name: Type check + run: pnpm --filter blog exec tsc --noEmit + + - name: Unit tests (Jest) + run: pnpm --filter blog test + + # E2E용 Chromium과 OS 의존성을 설치한다. + - name: Install Playwright browser + run: pnpm --filter blog exec playwright install --with-deps chromium + + # CI=true가 자동 주입되므로 playwright.config가 프로덕션 빌드(build && start)로 검증한다. + - name: E2E tests (Playwright) + run: pnpm --filter blog test:e2e + + # 실패 시 분석용 HTML 리포트(트레이스 포함)를 아티팩트로 올린다. + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: apps/blog/playwright-report/ + retention-days: 7 + if-no-files-found: ignore diff --git a/apps/blog/e2e/responsive.spec.ts b/apps/blog/e2e/responsive.spec.ts index fb8d856..a7dd9d0 100644 --- a/apps/blog/e2e/responsive.spec.ts +++ b/apps/blog/e2e/responsive.spec.ts @@ -11,12 +11,12 @@ test.describe('모바일 반응형', () => { // 데스크톱 전용 Tags 네비 링크는 md 미만에서 숨겨져야 한다. await expect(page.getByRole('link', { name: 'Tags' })).toBeHidden(); - // 햄버거(메뉴) 아이콘을 눌러 사이드바를 연다. - await page.locator('.tabler-icon-menu-2').click(); + // 햄버거(메뉴) 버튼을 눌러 사이드바를 연다(#87에서 추가된 접근성 라벨 사용). + await page.getByRole('button', { name: '메뉴 열기' }).click(); - // 닫혀 있을 땐 화면 밖(translate-x-full)에 있던 사이드바의 Series 섹션 단락이 + // 닫혀 있을 땐 화면 밖(translate-x-full)에 있던 사이드바 패널(dialog)이 // 열린 뒤에는 뷰포트 안으로 들어와야 한다(패널이 실제로 슬라이드인됐음을 검증). - const sidebarSeries = page.getByRole('paragraph').filter({ hasText: /^Series$/ }); - await expect(sidebarSeries).toBeInViewport(); + const sidebar = page.getByRole('dialog', { name: '사이트 메뉴' }); + await expect(sidebar).toBeInViewport(); }); }); diff --git a/apps/blog/playwright.config.ts b/apps/blog/playwright.config.ts index 9192557..b63e3d9 100644 --- a/apps/blog/playwright.config.ts +++ b/apps/blog/playwright.config.ts @@ -13,8 +13,8 @@ export default defineConfig({ timeout: 60_000, // CI에서만 실패 1회 재시도로 일시적 플레이키를 흡수한다(로컬은 즉시 실패가 디버깅에 유리). retries: process.env.CI ? 1 : 0, - // 실패 원인 추적용 리포터. 로컬에서 결과를 바로 열어볼 수 있게 한다. - reporter: process.env.CI ? 'github' : 'list', + // 리포터: 로컬은 간결한 list, CI는 GitHub 어노테이션 + 실패 분석용 HTML 리포트(아티팩트 업로드용). + reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list', use: { // 모든 테스트가 동일 출처를 바라보도록 기준 URL을 둔다(상대경로 goto 가능). baseURL: 'http://localhost:3000',