diff --git a/.eslintrc.json b/.eslintrc.json index df03548d..0f3f5255 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -71,6 +71,12 @@ "rules": { "import/default": "off" } + }, + { + "files": ["*.stories.tsx"], + "rules": { + "react/display-name": "off" + } } ] } diff --git a/.github/ISSUE_TEMPLATE/component-template.md b/.github/ISSUE_TEMPLATE/component-template.md new file mode 100644 index 00000000..2b88aa24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/component-template.md @@ -0,0 +1,46 @@ +--- +name: Component Template +about: Describe this issue template's purpose here. +title: "컴포넌트 이슈 작성 템플릿" +labels: "🎯 기능 구현" +assignees: "" +--- + +### **용도 (Usage)** + + + +### **기능 (Props)** + + + +### **참고 (Reference)** + + + +### **마일스톤 (Milestones)** + + diff --git a/.github/PULL_REQUEST_TEMPLATE/component_template.md b/.github/PULL_REQUEST_TEMPLATE/component_template.md new file mode 100644 index 00000000..d84374e9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/component_template.md @@ -0,0 +1,26 @@ + + +### 예상 동작 (Expected Behavior) + + + +### 고민했던 내용 (Considerations) + + + +### 관련 이슈 + +- resolved #(issue_num) diff --git a/.github/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md similarity index 100% rename from .github/pull_request_template.md rename to .github/PULL_REQUEST_TEMPLATE/pull_request_template.md diff --git a/.github/workflows/chromatic_auto_deploy.yml b/.github/workflows/chromatic_auto_deploy.yml index e0072257..432e77d5 100644 --- a/.github/workflows/chromatic_auto_deploy.yml +++ b/.github/workflows/chromatic_auto_deploy.yml @@ -25,18 +25,25 @@ jobs: with: node-version: 20 - - name: Corepack 활성화 - run: corepack enable - - - name: pnpm 설치 - run: corepack prepare pnpm@latest --activate + - name: pnpm 8.15.6 설치 + uses: pnpm/action-setup@v2 + with: + version: 8.15.6 - name: 의존성 설치 run: pnpm install --no-frozen-lockfile + - name: Check if token exists + run: | + if [ -n "${{ secrets.PRIMITIVE_UI_CHROMATIC_TOKEN }}" ]; then + echo "PRIMITIVE_UI_CHROMATIC_TOKEN is set" + else + echo "PRIMITIVE_UI_CHROMATIC_TOKEN is not set" + fi + - name: chromatic에 배포 id: publish_chromatic - uses: chromaui/action@v1 + uses: chromaui/action@latest with: projectToken: ${{ secrets.PRIMITIVE_UI_CHROMATIC_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/label-vrt-update.yml b/.github/workflows/label-vrt-update.yml new file mode 100644 index 00000000..af61811a --- /dev/null +++ b/.github/workflows/label-vrt-update.yml @@ -0,0 +1,73 @@ +name: VRT 스냅샷 업데이트 +on: + pull_request: + types: [labeled] + +jobs: + update-snapshots: + if: github.event.label.name == 'VRT' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v2 + with: + version: 8.15.6 + + - name: 의존성 설치 + run: pnpm install + + - name: Playwright 설치 + run: pnpm exec playwright install --with-deps + + - name: 스토리북 빌드 + run: pnpm run build-storybook + + working-directory: packages/primitive + - name: 스토리북 실행 + run: | + npx serve -l 6006 packages/primitive/storybook-static & + echo $! > .storybook-pid + + - name: 스냅샷 업데이트 + run: pnpm run e2e:update + + - name: 스토리북 프로세스 종료 + if: always() + run: kill $(cat .storybook-pid) + + - name: 변경된 스냅샷 커밋 및 푸시 + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add .playwright + git diff --staged --quiet || git commit -m "Update VRT snapshots in .playwright folder" + git push origin HEAD:${{ github.head_ref }} + + - name: PR 코멘트 작성 + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '## VRT 스냅샷 업데이트 완료\n\n스냅샷이 성공적으로 업데이트되었습니다. 변경된 스냅샷이 이 PR에 포함되었습니다. 리뷰해주세요.' + }) + + - name: VRT 레이블 제거 + uses: actions/github-script@v6 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.removeLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: 'VRT' + }) diff --git a/.github/workflows/pr-dday-labeler.yml b/.github/workflows/pr-dday-labeler.yml new file mode 100644 index 00000000..953a767f --- /dev/null +++ b/.github/workflows/pr-dday-labeler.yml @@ -0,0 +1,22 @@ +name: PR D-day Labeler + +on: + schedule: + - cron: "0 15 * * *" + pull_request: + types: [opened, reopened] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + label-prs: + runs-on: ubuntu-latest + steps: + - name: PR D-day Labeler + uses: team-warrr/pr-dday-labeler@1.0.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/pr-vrt.yml b/.github/workflows/pr-vrt.yml new file mode 100644 index 00000000..3591fa76 --- /dev/null +++ b/.github/workflows/pr-vrt.yml @@ -0,0 +1,84 @@ +name: PR VRT 테스트 +on: + pull_request: + branches: [develop] + +permissions: + contents: read + pull-requests: write + +jobs: + vrt-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: pnpm/action-setup@v2 + with: + version: 8.15.6 + + - name: 의존성 설치 + run: pnpm install --no-frozen-lockfile + + - name: Playwright 설치 + run: pnpm exec playwright install --with-deps + + - name: 스토리북 빌드 + run: pnpm run build-storybook + working-directory: packages/primitive + + - name: 스토리북 실행 + run: | + npx serve -l 6006 packages/primitive/storybook-static & + echo $! > .storybook-pid + + - name: VRT 테스트 실행 + id: vrt-test + run: | + if pnpm run e2e; then + echo "결과=성공" >> $GITHUB_OUTPUT + else + echo "결과=실패" >> $GITHUB_OUTPUT + fi + + - name: 스토리북 프로세스 종료 + if: always() + run: kill $(cat .storybook-pid) + + - name: 테스트 결과 및 diff 이미지 업로드 + uses: actions/upload-artifact@v4 + if: failure() + with: + name: vrt-results + path: | + playwright-report/ + .playwright/ + retention-days: 7 + + - name: PR 코멘트 작성 (성공) + uses: actions/github-script@v6 + if: success() + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '## VRT 테스트 성공\n\nVRT 테스트가 성공적으로 완료되었습니다.' + }) + + - name: PR 코멘트 작성 (실패) + uses: actions/github-script@v6 + if: failure() + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '## VRT 테스트 실패\n\nVRT 테스트가 실패했습니다. 자세한 내용은 첨부된 테스트 결과와 diff 이미지를 확인해주세요.\n\n[테스트 결과 확인](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}})\n\n스냅샷 업데이트가 필요한 경우, PR에 "VRT" 레이블을 추가해주세요.' + }) diff --git a/.gitignore b/.gitignore index a341f14e..f8420906 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,10 @@ yarn-error.log* .DS_Store *.pem -*storybook.log \ No newline at end of file +*storybook.log +.eslintcache +local.secrets +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..9fc3832c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,10 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +pnpm lint + +pnpm test +if [ $? -ne 0 ]; then + echo "테스트 실패" + exit 1 +fi diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100644 index 00000000..80fabd3a --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +pnpm format diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 00000000..31a2100b --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,3 @@ +{ + "**.*.{ts,tsx}": ["pnpm lint", "pnpm prettier"] +} diff --git a/.npmrc b/.npmrc index e69de29b..b6f27f13 100644 --- a/.npmrc +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.playwright/report/index.html b/.playwright/report/index.html new file mode 100644 index 00000000..dd196f23 --- /dev/null +++ b/.playwright/report/index.html @@ -0,0 +1,68 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/.playwright/results.json b/.playwright/results.json new file mode 100644 index 00000000..4439599b --- /dev/null +++ b/.playwright/results.json @@ -0,0 +1,320 @@ +{ + "config": { + "configFile": "/home/runner/work/warrr-ui/warrr-ui/playwright.config.ts", + "rootDir": "/home/runner/work/warrr-ui/warrr-ui/e2e", + "forbidOnly": true, + "fullyParallel": true, + "globalSetup": null, + "globalTeardown": null, + "globalTimeout": 0, + "grep": {}, + "grepInvert": null, + "maxFailures": 0, + "metadata": { + "actualWorkers": 1 + }, + "preserveOutput": "always", + "reporter": [ + [ + "line", + null + ], + [ + "blob", + null + ], + [ + "json", + { + "outputFile": "/home/runner/work/warrr-ui/warrr-ui/.playwright/results.json" + } + ] + ], + "reportSlowTests": { + "max": 5, + "threshold": 15000 + }, + "quiet": false, + "projects": [ + { + "outputDir": "/home/runner/work/warrr-ui/warrr-ui/.playwright/results", + "repeatEach": 1, + "retries": 2, + "metadata": {}, + "id": "chromium", + "name": "chromium", + "testDir": "/home/runner/work/warrr-ui/warrr-ui/e2e", + "testIgnore": [], + "testMatch": [ + "**/*.@(spec|test).?(c|m)[jt]s?(x)" + ], + "timeout": 30000 + }, + { + "outputDir": "/home/runner/work/warrr-ui/warrr-ui/.playwright/results", + "repeatEach": 1, + "retries": 2, + "metadata": {}, + "id": "firefox", + "name": "firefox", + "testDir": "/home/runner/work/warrr-ui/warrr-ui/e2e", + "testIgnore": [], + "testMatch": [ + "**/*.@(spec|test).?(c|m)[jt]s?(x)" + ], + "timeout": 30000 + }, + { + "outputDir": "/home/runner/work/warrr-ui/warrr-ui/.playwright/results", + "repeatEach": 1, + "retries": 2, + "metadata": {}, + "id": "webkit", + "name": "webkit", + "testDir": "/home/runner/work/warrr-ui/warrr-ui/e2e", + "testIgnore": [], + "testMatch": [ + "**/*.@(spec|test).?(c|m)[jt]s?(x)" + ], + "timeout": 30000 + } + ], + "shard": null, + "updateSnapshots": "all", + "version": "1.46.0", + "workers": 1, + "webServer": null + }, + "suites": [ + { + "title": "components/Button.test.ts", + "file": "components/Button.test.ts", + "column": 0, + "line": 0, + "specs": [], + "suites": [ + { + "title": "Button 컴포넌트", + "file": "components/Button.test.ts", + "line": 6, + "column": 6, + "specs": [ + { + "title": "primary 시각적 회귀 테스트", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 30000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "chromium", + "projectName": "chromium", + "results": [ + { + "workerIndex": 0, + "status": "passed", + "duration": 1034, + "errors": [], + "stdout": [ + { + "text": "/home/runner/work/warrr-ui/warrr-ui/.playwright/snapshots/components/Button.test.ts-snapshots/Button-컴포넌트-primary-시각적-회귀-테스트-1-chromium-linux.png is re-generated, writing actual.\n" + } + ], + "stderr": [], + "retry": 0, + "startTime": "2024-08-15T21:13:25.127Z", + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "ae6d8ccd909362ca9a3d-f411095b4d7a7207787e", + "file": "components/Button.test.ts", + "line": 7, + "column": 7 + }, + { + "title": "axe를 사용하여 자동 접근성 테스트", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 30000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "chromium", + "projectName": "chromium", + "results": [ + { + "workerIndex": 0, + "status": "passed", + "duration": 809, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2024-08-15T21:13:26.431Z", + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "ae6d8ccd909362ca9a3d-88976050552a720a57fd", + "file": "components/Button.test.ts", + "line": 15, + "column": 7 + }, + { + "title": "primary 시각적 회귀 테스트", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 30000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "firefox", + "projectName": "firefox", + "results": [ + { + "workerIndex": 1, + "status": "passed", + "duration": 1843, + "errors": [], + "stdout": [ + { + "text": "/home/runner/work/warrr-ui/warrr-ui/.playwright/snapshots/components/Button.test.ts-snapshots/Button-컴포넌트-primary-시각적-회귀-테스트-1-firefox-linux.png is re-generated, writing actual.\n" + } + ], + "stderr": [], + "retry": 0, + "startTime": "2024-08-15T21:13:27.729Z", + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "ae6d8ccd909362ca9a3d-9b41d4ab3bf8ed6fb360", + "file": "components/Button.test.ts", + "line": 7, + "column": 7 + }, + { + "title": "axe를 사용하여 자동 접근성 테스트", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 30000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "firefox", + "projectName": "firefox", + "results": [ + { + "workerIndex": 1, + "status": "passed", + "duration": 917, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2024-08-15T21:13:30.511Z", + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "ae6d8ccd909362ca9a3d-80d45ed3fa2dbcc14d45", + "file": "components/Button.test.ts", + "line": 15, + "column": 7 + }, + { + "title": "primary 시각적 회귀 테스트", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 30000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "webkit", + "projectName": "webkit", + "results": [ + { + "workerIndex": 2, + "status": "passed", + "duration": 4167, + "errors": [], + "stdout": [ + { + "text": "/home/runner/work/warrr-ui/warrr-ui/.playwright/snapshots/components/Button.test.ts-snapshots/Button-컴포넌트-primary-시각적-회귀-테스트-1-webkit-linux.png is re-generated, writing actual.\n" + } + ], + "stderr": [], + "retry": 0, + "startTime": "2024-08-15T21:13:32.377Z", + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "ae6d8ccd909362ca9a3d-d425228141c78bf583f3", + "file": "components/Button.test.ts", + "line": 7, + "column": 7 + }, + { + "title": "axe를 사용하여 자동 접근성 테스트", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 30000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "webkit", + "projectName": "webkit", + "results": [ + { + "workerIndex": 2, + "status": "passed", + "duration": 1028, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2024-08-15T21:13:38.735Z", + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "ae6d8ccd909362ca9a3d-5120bb9063b0336c94ab", + "file": "components/Button.test.ts", + "line": 15, + "column": 7 + } + ] + } + ] + } + ], + "errors": [], + "stats": { + "startTime": "2024-08-15T21:13:24.566Z", + "duration": 15243.547, + "expected": 6, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } +} \ No newline at end of file diff --git a/.playwright/results/.last-run.json b/.playwright/results/.last-run.json new file mode 100644 index 00000000..cbcc1fba --- /dev/null +++ b/.playwright/results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git "a/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-chromium/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" "b/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-chromium/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" new file mode 100644 index 00000000..5a25c681 Binary files /dev/null and "b/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-chromium/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" differ diff --git "a/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-firefox/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" "b/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-firefox/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" new file mode 100644 index 00000000..ecd6556b Binary files /dev/null and "b/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-firefox/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" differ diff --git "a/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-webkit/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" "b/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-webkit/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" new file mode 100644 index 00000000..3321bdd3 Binary files /dev/null and "b/.playwright/results/components-Button-Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-webkit/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-actual.png" differ diff --git "a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-chromium-linux.png" "b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-chromium-linux.png" new file mode 100644 index 00000000..5a25c681 Binary files /dev/null and "b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-chromium-linux.png" differ diff --git "a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-firefox-linux.png" "b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-firefox-linux.png" new file mode 100644 index 00000000..ecd6556b Binary files /dev/null and "b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-firefox-linux.png" differ diff --git "a/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-webkit-linux.png" "b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-webkit-linux.png" new file mode 100644 index 00000000..3321bdd3 Binary files /dev/null and "b/.playwright/snapshots/components/Button.test.ts-snapshots/Button-\354\273\264\355\217\254\353\204\214\355\212\270-primary-\354\213\234\352\260\201\354\240\201-\355\232\214\352\267\200-\355\205\214\354\212\244\355\212\270-1-webkit-linux.png" differ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..3250cb23 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +turbo/ +pnpm-lock.yaml +.playwright/report/index.html +.playwright/results.json +.playwright/results/.last-run.json diff --git a/.prettierrc.json b/.prettierrc.json index 3e38326e..1554e0e3 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -7,7 +7,7 @@ "printWidth": 100, "arrowParens": "always", "bracketSpacing": true, - "jsxBracketSameLine": false, + "bracketSameLine": false, "jsxSingleQuote": false, "quoteProps": "as-needed" } diff --git a/.vscode/settings.json b/.vscode/settings.json index 1e0fc28e..51beef4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,6 @@ "source.fixAll.eslint": "explicit", "source.organizeImports": "explicit" }, + "prettier.requireConfig": true, "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/e2e/components/Button.test.ts b/e2e/components/Button.test.ts new file mode 100644 index 00000000..32b086a4 --- /dev/null +++ b/e2e/components/Button.test.ts @@ -0,0 +1,23 @@ +import { Page, expect, test } from "@playwright/test"; + +import { axeAccessibilityScan } from "../test-utils/a11y"; +import { visit } from "../test-utils/storybook"; + +test.describe("Button 컴포넌트", () => { + test("primary 시각적 회귀 테스트", async ({ page }: { page: Page }) => { + await visit(page, { + id: "example-button--primary", + }); + + await expect(page).toHaveScreenshot(); + }); + + test("axe를 사용하여 자동 접근성 테스트", async ({ page }: { page: Page }) => { + await visit(page, { + id: "example-button--primary", + }); + + const accessibilityScanResults = await axeAccessibilityScan(page); + expect(accessibilityScanResults.violations).toEqual([]); + }); +}); diff --git a/e2e/test-utils/a11y.ts b/e2e/test-utils/a11y.ts new file mode 100644 index 00000000..6063a1ac --- /dev/null +++ b/e2e/test-utils/a11y.ts @@ -0,0 +1,12 @@ +import AxeBuilder from "@axe-core/playwright"; +import { Page } from "@playwright/test"; +import { AxeResults } from "axe-core"; + +export async function axeAccessibilityScan(page: Page): Promise { + const accessibilityScanResults = await new AxeBuilder({ page }) + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) + .disableRules(["color-contrast"]) + .analyze(); + + return accessibilityScanResults; +} diff --git a/e2e/test-utils/storybook.ts b/e2e/test-utils/storybook.ts new file mode 100644 index 00000000..d1241f9b --- /dev/null +++ b/e2e/test-utils/storybook.ts @@ -0,0 +1,77 @@ +import { Page } from "@playwright/test"; + +type Value = + | string + | boolean + | number + | { + [Key: string]: Value; + }; + +interface Options { + id: string; + args?: Record; + globals?: Record; +} + +const { STORYBOOK_URL = "http://localhost:6006" } = process.env; + +export async function visit(page: Page, options: Options) { + const { id, args, globals } = options; + const url = process.env.CI + ? new URL(`${STORYBOOK_URL}/iframe`) + : new URL(`${STORYBOOK_URL}/iframe.html`); + + url.searchParams.set("id", id); + url.searchParams.set("viewMode", "story"); + + if (args) { + const serializedArgs = Object.entries(args) + .map(([key, value]) => `${key}:${value}`) + .join(";"); + url.searchParams.set("args", serializedArgs); + } + + if (globals) { + let params = ""; + for (const [key, value] of Object.entries(globals)) { + if (params !== "") { + params += ";"; + } + if (typeof value === "object") { + params += serializeObject(value, key); + } else { + params += `${key}:${value}`; + } + } + url.searchParams.set("globals", params); + } + + await page.goto(url.toString()); + + // 스토리북이 로딩될 때까지 대기하여야 playwright 테스트 가능 + await page.waitForSelector("body.sb-show-main:not(.sb-show-preparing-story)"); + await page.waitForSelector("#storybook-root > *"); +} + +function serializeObject( + object: T, + parentPath: string +): string { + return Object.entries(object) + .map(([key, value]) => { + if (typeof value === "object") { + return serializeObject(value, `${parentPath}.${key}`); + } + return `${parentPath}.${key}:${serialize(value)}`; + }) + .join(";"); +} + +function serialize(value: Value): string { + if (typeof value === "boolean") { + return `!${value}`; + } + + return `${value}`; +} diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 00000000..fc8044df --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,10 @@ +import type { Config } from "jest"; + +const config: Config = { + testEnvironment: "jsdom", + preset: "ts-jest", + testMatch: ["/packages/**/?(*.)+(spec|test).[jt]s?(x)"], + setupFilesAfterEnv: ["/jest.setup.ts"], +}; + +export default config; diff --git a/jest.setup.ts b/jest.setup.ts new file mode 100644 index 00000000..8087db30 --- /dev/null +++ b/jest.setup.ts @@ -0,0 +1,2 @@ +import "@testing-library/dom"; +import "@testing-library/jest-dom"; diff --git a/package.json b/package.json index 5b489b65..b5666479 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,51 @@ { - "name": "warrr-ui", + "name": "@warrr-ui", "private": true, "scripts": { "build": "turbo build", "dev": "turbo dev", - "prettier": "exec prettier . --write", - "lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix" + "format": "prettier . --write --cache", + "lint": "eslint . --ext .ts,.tsx --fix --cache", + "turbo:gen": "turbo gen", + "prepare": "husky", + "e2e": "playwright test", + "e2e:update": "playwright test --update-snapshots", + "test": "jest --verbose --config ./jest.config.ts" }, "devDependencies": { + "@axe-core/playwright": "^4.9.1", + "@playwright/test": "^1.46.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@turbo/gen": "^2.0.9", + "@types/jest": "^29.5.12", + "@types/node": "^20.14.9", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", + "axe-core": "^4.10.0", "eslint": "8.57.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", + "husky": "^9.1.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "lint-staged": "^15.2.7", "prettier": "^3.2.5", + "ts-jest": "^29.2.3", + "ts-node": "^10.9.2", + "tsup": "^8.2.1", "turbo": "^2.0.4", - "typescript": "^5.4.5", - "@types/node": "^20.14.9", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0" + "typescript": "^5.4.5" }, "packageManager": "pnpm@8.15.6", "engines": { - "node": ">=18" + "node": ">=18", + "pnpm": "8.15.6" } } diff --git a/packages/plugins/boilerplate/figma.manifest.ts b/packages/plugins/boilerplate/src/config/figma.manifest.ts similarity index 100% rename from packages/plugins/boilerplate/figma.manifest.ts rename to packages/plugins/boilerplate/src/config/figma.manifest.ts diff --git a/packages/plugins/boilerplate/injectBundledJsIntoHTML.ts b/packages/plugins/boilerplate/src/config/injectBundledJsIntoHTML.ts similarity index 100% rename from packages/plugins/boilerplate/injectBundledJsIntoHTML.ts rename to packages/plugins/boilerplate/src/config/injectBundledJsIntoHTML.ts diff --git a/packages/plugins/boilerplate/tsconfig.json b/packages/plugins/boilerplate/tsconfig.json index 2d4a552d..c108e49b 100644 --- a/packages/plugins/boilerplate/tsconfig.json +++ b/packages/plugins/boilerplate/tsconfig.json @@ -5,5 +5,6 @@ "typeRoots": ["./node_modules/@types", "./node_modules/@figma"], "skipLibCheck": true, "allowSyntheticDefaultImports": true - } + }, + "include": ["src", "vite.config.ts"] } diff --git a/packages/plugins/boilerplate/vite.config.ts b/packages/plugins/boilerplate/vite.config.ts index 2c2e4ab9..6a04c9e2 100644 --- a/packages/plugins/boilerplate/vite.config.ts +++ b/packages/plugins/boilerplate/vite.config.ts @@ -4,8 +4,8 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import generateFile from "vite-plugin-generate-file"; -import manifest from "./figma.manifest"; -import { injectBundledJsIntoHTML } from "./injectBundledJsIntoHTML"; +import manifest from "./src/config/figma.manifest"; +import { injectBundledJsIntoHTML } from "./src/config/injectBundledJsIntoHTML"; export default defineConfig({ plugins: [ diff --git a/packages/primitive/components/Button.test.tsx b/packages/primitive/components/Button.test.tsx new file mode 100644 index 00000000..15284344 --- /dev/null +++ b/packages/primitive/components/Button.test.tsx @@ -0,0 +1,17 @@ +import { render } from "@testing-library/react"; + +import { Button } from "./Button"; + +describe("버튼 컴포넌트 테스트", () => { + it("렌더링이 올바르게 되는지 확인합니다.", () => { + const wrapper = render(