From a54b175e349b207fb88b92b32773527c0f8f17a7 Mon Sep 17 00:00:00 2001 From: MJ <80272444+minai621@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:10:00 +0900 Subject: [PATCH] =?UTF-8?q?chore/#38=20playwright=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: playwright 설치 * feat: playwright e2e 테스트 (시각적 회귀 테스트, 웹접근성 테스트) * chore: playwright ci 테스트 * chore: ci 환경에서 의존성 설치 방식 변경 * chore: 워크플로우 ci환경에 playwright 브라우저 설치 * chore: 워크플로우 playwright ci 옵션 추가 * chore: playwright npx로 실행 명령어 변경 * chore: playwright --project 옵션 제거 * chore: 명령어 복구 * chore: playwright 환경 수정 * chore: playwright worker 옵션 변경 * chore: log 추가 * chore: 스토리북 빌드 방식으로 실행 * chore: button 스냅샷 업로드 * chore: playwright 설정 복원 * chore: pr comment 추가 * chore: fetch-depth 추가 * chore: 스냅샷 파일 수정 * chore: testDir path 설정 * chore: playwright 옵션 변경 * chore: 불필요한 테스트 삭제 * chore: checkout version 업데이트 * chore: 테스트 코드 변경 * chore: log 삭제 * chore: error 로그 추가 * chore: 로그 추가 * chore: --update-snapshots 옵션 추가 * chore: 폴더 확인 로직 수정 * chore: yml indent 수정 * chore: 디렉토리 확인 수정 * chore: update snapshot 옵션 삭제 * chore: 시각적 변경 테스트 * chore: update snapshot * chore: 디렉토리 구조 확인 * chore: 아티팩트 업로드 * chore: bold 처리 삭제 * chore: button font arial로 변경 * chore: font 업데이트 이후 actual 변경 * chore: font-weight 700 to bold * chore: web font로 변경 * chore: 폰트 로딩 상태 확인 * chore: roboto로 변경 * chore: font-weight 삭제 * chore: ci 환경에 폰트 설치 * chore: 변경되는 스냅샷 확인 * chore: plarywright 스크립트 변경 * chore: storybook 실행 명령어 수정 * chore: process 변경 * chore: local vrt 삭제 * chore: label flow 수정 * chore: .playwright의 하위에 있는 폴더만 pr에 반영되도록 수정 * Update VRT snapshots in .playwright folder * chore: button에 불필요한 텍스트 삭제 * Update VRT snapshots in .playwright folder * chore: Button 라인 분리 * chore: 실패시 아티팩트 업로드하도록 수정 * chore: 성공 comment 메시지 수정 * chore: chromatic action version 수정 * chore: pnpm action version 변경 * chore: vrt test에 permission 추가 * chore: 토큰 체커 --------- Co-authored-by: GitHub Action --- .github/workflows/chromatic_auto_deploy.yml | 19 +- .github/workflows/label-vrt-update.yml | 73 ++++ .github/workflows/pr-vrt.yml | 84 +++++ .gitignore | 4 + .playwright/report/index.html | 68 ++++ .playwright/results.json | 320 ++++++++++++++++++ .playwright/results/.last-run.json | 4 + ...\214\354\212\244\355\212\270-1-actual.png" | Bin 0 -> 5834 bytes ...\214\354\212\244\355\212\270-1-actual.png" | Bin 0 -> 20332 bytes ...\214\354\212\244\355\212\270-1-actual.png" | Bin 0 -> 5873 bytes ...\212\244\355\212\270-1-chromium-linux.png" | Bin 0 -> 5834 bytes ...4\212\244\355\212\270-1-firefox-linux.png" | Bin 0 -> 20332 bytes ...54\212\244\355\212\270-1-webkit-linux.png" | Bin 0 -> 5873 bytes e2e/components/Button.test.ts | 23 ++ e2e/test-utils/a11y.ts | 12 + e2e/test-utils/storybook.ts | 77 +++++ package.json | 5 + packages/primitive/components/Button.tsx | 3 +- playwright.config.ts | 69 ++++ tsconfig.json | 2 +- 20 files changed, 755 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/label-vrt-update.yml create mode 100644 .github/workflows/pr-vrt.yml create mode 100644 .playwright/report/index.html create mode 100644 .playwright/results.json create mode 100644 .playwright/results/.last-run.json create mode 100644 ".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" create mode 100644 ".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" create mode 100644 ".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" create mode 100644 ".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" create mode 100644 ".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" create mode 100644 ".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" create mode 100644 e2e/components/Button.test.ts create mode 100644 e2e/test-utils/a11y.ts create mode 100644 e2e/test-utils/storybook.ts create mode 100644 playwright.config.ts 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-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 bd7f44fc..f716e182 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ yarn-error.log* *storybook.log .eslintcache +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ 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 0000000000000000000000000000000000000000..5a25c68173aaa3cd8f6c213b9899eb4f4a763a17 GIT binary patch literal 5834 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5a25c68173aaa3cd8f6c213b9899eb4f4a763a17 GIT binary patch literal 5834 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 literal 0 HcmV?d00001 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/package.json b/package.json index 9b240a43..1ba044c7 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,13 @@ "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/react": "^16.0.0", "@turbo/gen": "^2.0.9", "@types/jest": "^29.5.12", @@ -19,6 +23,7 @@ "@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", diff --git a/packages/primitive/components/Button.tsx b/packages/primitive/components/Button.tsx index 1e39f9fe..8d61c627 100644 --- a/packages/primitive/components/Button.tsx +++ b/packages/primitive/components/Button.tsx @@ -32,13 +32,14 @@ export const Button = ({ ...props }: ButtonProps) => { const mode = primary ? "storybook-button--primary" : "storybook-button--secondary"; + return (