Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e6f45d5
fix: keep the check mark on a completed task while dragging it
pangil5634 Oct 25, 2026
8fa53e9
fix: apply reordered routines to the record page cache
pangil5634 Oct 25, 2026
e5e62a0
feat: connect to firebase emulators in e2e mode
pangil5634 Oct 25, 2026
405421f
chore: set up playwright e2e tests with smoke cases
pangil5634 Oct 25, 2026
29851d0
test: add seed and drag helpers for e2e tests
pangil5634 Oct 25, 2026
be7a331
test: add regression e2e cases for routine reorder and task drag
pangil5634 Oct 25, 2026
47c8938
feat: ask before deleting a task or routine and ending a category
pangil5634 Oct 25, 2026
0d6f541
fix: keep the confirm button width while processing
pangil5634 Oct 25, 2026
2bf74c8
fix: keep presses inside a modal from starting a drag behind it
pangil5634 Oct 25, 2026
6251dec
test: propose e2e cases for every page
pangil5634 Oct 25, 2026
0971d96
feat: give accessible names to task, routine, calendar and modal cont…
pangil5634 Oct 25, 2026
c1a59e6
fix: show the failure message when saving a completion fails offline
pangil5634 Oct 25, 2026
aefdf40
test: add home p1 e2e cases
pangil5634 Oct 25, 2026
0abeb79
test: add routine p1 e2e cases
pangil5634 Oct 27, 2026
5a8a598
fix: refresh the home routine table when a category changes
pangil5634 Oct 27, 2026
5d3337e
fix: draw week board tasks in the category's current color
pangil5634 Oct 27, 2026
097f5bc
test: add category p1 e2e cases
pangil5634 Oct 27, 2026
39e306a
feat: name date picker days with the full date
pangil5634 Oct 27, 2026
19295f3
test: add record p1 e2e cases
pangil5634 Oct 27, 2026
1876862
test: record the monthly stats creation race as a case
pangil5634 Oct 27, 2026
46f9dc4
test: add my page and common p1 e2e cases
pangil5634 Oct 27, 2026
ff96839
fix: keep edits made while a month's stats are first created
pangil5634 Oct 27, 2026
4b41f1d
test: cover edits made while a month's stats are first created
pangil5634 Oct 27, 2026
f808602
test: pin firebase-tools and steady e2e runs under load
pangil5634 Oct 27, 2026
13a30f4
ci: run checks and e2e tests on pull requests and main
pangil5634 Oct 27, 2026
b050714
test: give unit tests their own demo firebase config
pangil5634 Oct 27, 2026
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
13 changes: 13 additions & 0 deletions .env.e2e
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# E2E 테스트 전용 설정 (`vite --mode e2e`)
#
# .env 의 실제 프로젝트 값을 덮어써서, 테스트가 어떤 경우에도 운영 데이터에 닿지 않게 한다.
# demo- 로 시작하는 프로젝트 ID 는 Firebase 에뮬레이터 전용이라 실제 프로젝트와 연결될 수 없다.
# 값이 비밀이 아니므로 저장소에 올린다.

VITE_FIREBASE_API_KEY=e2e-fake-api-key
VITE_FIREBASE_AUTH_DOMAIN=demo-checky.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=demo-checky
VITE_FIREBASE_STORAGE_BUCKET=demo-checky.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=0
VITE_FIREBASE_APP_ID=e2e-fake-app-id
VITE_ADMIN_SHORTCUT_TAP_COUNT=
12 changes: 12 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# 단위 테스트 전용 설정 (vitest 는 test 모드로 돌아 이 파일을 읽는다)
#
# .env 의 실제 프로젝트 값을 덮어써, 단위 테스트가 어떤 경우에도 운영 프로젝트 설정을 쓰지 않게 한다.
# .env 가 없는 CI 에서도 Firebase 초기화가 실패하지 않는다. 값이 비밀이 아니므로 저장소에 올린다.

VITE_FIREBASE_API_KEY=unit-test-fake-api-key
VITE_FIREBASE_AUTH_DOMAIN=demo-checky.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=demo-checky
VITE_FIREBASE_STORAGE_BUCKET=demo-checky.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=0
VITE_FIREBASE_APP_ID=unit-test-fake-app-id
VITE_ADMIN_SHORTCUT_TAP_COUNT=
67 changes: 67 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# PR 과 main 푸시마다 검사(타입·린트·단위 테스트)와 E2E 를 돌린다.
# E2E 는 Firebase 에뮬레이터 위에서 돌아 실제 프로젝트에 닿지 않고, 비밀값도 쓰지 않는다.
name: test

on:
pull_request:
push:
branches: [main]

# 같은 브랜치에 새로 푸시하면 앞선 실행은 취소한다
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run check

e2e:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
# firebase-tools 의 에뮬레이터는 JDK 21 이상이 필요하다
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
- run: npm ci

# 에뮬레이터 실행 파일은 firebase-tools 버전마다 받으므로 그 버전으로 캐시한다
- id: firebase
run: echo "version=$(node -p "require('firebase-tools/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Firebase emulators
uses: actions/cache@v6
with:
path: ~/.cache/firebase/emulators
key: firebase-emulators-${{ steps.firebase.outputs.version }}

- run: npx playwright install --with-deps chromium
- run: npm run e2e

# 실패하면 각 단계의 화면·기록(trace)을 내려받아 볼 수 있게 남긴다
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 14
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ AGENTS.md
# 내부 개발 문서 (성능 리포트, 리팩터링 기록, 릴리즈 노트, 배포 메모 등)
# 화면에 표시되는 개인정보 처리방침·이용약관은 src/pages/legal/content 에 있다
docs/

# Playwright 실행 결과
/test-results/
/playwright-report/
/blob-report/
85 changes: 85 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# E2E 테스트

Playwright 로 실제 화면을 띄워 페이지·기능을 검증한다.
앱은 Firebase 에뮬레이터(auth, firestore)에 붙어 돌아가므로 운영 데이터에 닿지 않는다.

## 실행

준비물: **JDK 21 이상** (firebase-tools 가 에뮬레이터를 띄울 때 요구한다).

```bash
npm run e2e # 에뮬레이터를 켜고 전체를 돌린 뒤 끈다
npm run e2e:ui # 에뮬레이터를 켜고 Playwright UI 창을 연다. 창을 닫으면 에뮬레이터도 꺼진다
```

케이스를 골라 돌리기, ID·태그로 거르기, 반복 실행은 UI 창 안에서 한다.

## CI

`.github/workflows/test.yml` 이 PR 과 main 푸시마다 `npm run check` 와 `npm run e2e` 를 돌린다.
CI 에서는 실패한 테스트를 두 번까지 다시 돌리고, 다시 돌려 통과한 테스트는 실패가 아니라 flaky 로 표시된다.
실패하면 실행 화면의 Artifacts 에서 `playwright-report` 를 내려받아 `npx playwright show-report playwright-report` 로 본다.
화면을 단계별로 되감아 보려면 `npx playwright show-trace <test-results 안의 trace.zip>` 를 쓴다.

## 어떻게 동작하나

| 문제 | 해결 |
| --- | --- |
| Google 팝업 로그인은 자동화할 수 없다 | `vite --mode e2e` 에서만 열리는 `window.__e2eSignIn` 으로 에뮬레이터에 가짜 Google 토큰을 넣는다 (`src/firebase/firebase.ts`). 배포 빌드에는 들어가지 않는다 |
| 운영 데이터 오염 | `.env.e2e` 가 프로젝트를 `demo-checky` 로 덮어쓴다. `demo-` 프로젝트는 에뮬레이터 전용이다 |
| 테스트끼리 데이터가 섞인다 | `signIn()` 이 테스트마다 새 사용자를 만든다. 데이터가 모두 `users/{uid}` 아래에 있어 병렬로 돌려도 서로 보지 못한다 |
| 화면이 조용히 깨진다 | 모든 테스트가 콘솔 에러·처리되지 않은 예외를 모아, 하나라도 있으면 실패한다 (`pageErrors` fixture) |

## 테스트 케이스 추가

**`cases/*.md` 가 기준이고, `specs/` 는 그 구현이다.** 화면 단위로 파일을 나눈다.

```md
### TC-ROUTINE-007 루틴 순서 변경이 기록 화면에 반영된다
- 우선순위: P1 / 태그: @regression
- 상태: 제안 | 확정 | 구현됨
- 전제: 같은 카테고리에 루틴 A, B, C
- 절차: 루틴 화면에서 C 를 맨 위로 끈다 → 기록 화면으로 이동
- 기대: 루틴별 수행 목록이 C, A, B 순서
- 출처: 8fa53e9 (회귀)
```

- **ID** 는 `TC-<화면>-<번호>` 로 정하고, spec 의 테스트 이름 앞에 그대로 쓴다. ID 로 케이스와 코드를 오간다.
- **우선순위**: P1 깨지면 출시 불가 / P2 주요 기능 / P3 그 밖
- **태그**: `@smoke` 화면이 열리는지 / `@regression` 고친 버그의 재발 방지
- **상태**: 코드에서 뽑아 낸 후보는 `제안` 으로 올리고, 사람이 확인한 뒤 `확정` → 구현하면 `구현됨`
- **출처**: 직접 추가, 버그 리포트, 커밋 해시 등 이 케이스가 왜 있는지

## 도구

| 파일 | 쓰임 |
| --- | --- |
| `fixtures.ts` | `test`, `expect` 는 `@playwright/test` 가 아니라 여기서 가져온다 |
| `seed.ts` | 시작 데이터 넣기. `const { seed } = await signIn()` 뒤에 `seed.category(...)`, `seed.routine(...)` 처럼 쓴다 |
| `drag.ts` | 끌어 놓기. `dragTo(page, 집을 것, 놓을 곳)`, 끄는 도중을 확인하려면 `grab()` |
| `emulator.ts` | `inspect.userDoc(uid)`, `inspect.userCollections(uid)`, `inspect.authUserExists(uid)` 관리자 권한으로 에뮬레이터 들여다보기(탈퇴 뒤 데이터 확인 등) |
| `network.ts` | `waitForSaved(page)` 저장이 끝날 때까지 기다리기(새로고침으로 확인하기 전에), `failWrites(page)` 쓰기 실패 흉내 |

```ts
test.use({ today: "2026-10-21" }); // 화면의 오늘을 고정

test("TC-ROUTINE-001 ...", { tag: "@regression" }, async ({ page, signIn }) => {
const { seed } = await signIn();
const category = await seed.category({ name: "건강" });
await seed.routine({ title: "독서", categoryId: category.id, orderIndex: 0, startDate: "2026-10-01" });
await page.goto("/routine");
// ...
});
```

## 작성 규칙

- 셀렉터는 `getByRole`, `getByLabel`, `getByText` 순으로 쓴다. 잡히지 않는 요소는 테스트에서 우회하지 말고 마크업에 레이블을 붙인다.
역할도 이름도 없는 요소(끌기 미리보기 같은 그림)에만 `data-testid` 를 단다.
- `signIn()` 은 로그인 전용 화면을 새로 열어 로그인한다. 같은 탭에서 계정을 바꾸는 흐름처럼 새로고침 없이 이어져야 하면 `signIn({ inPlace: true })` 를 쓴다.
- 헤더 메뉴로 화면을 옮긴 직후에는 새 화면이 뜬 것을 먼저 확인한다. 다음 화면 코드를 받는 동안 이전 화면이 그대로 남아 있어, 같은 이름의 요소를 잘못 잡는다.
- 고정 대기(`waitForTimeout`)는 쓰지 않는다. `expect(...).toBeVisible()` 처럼 상태를 기다린다. 예외는 `drag.ts` 의 누르고 있기뿐이다.
- 화면은 저장을 기다리지 않고 먼저 바뀐다. 새로고침이나 다른 화면에서 저장을 확인하려면 먼저 `waitForSaved(page)` 를 부른다.
- 일부러 실패를 일으켜 콘솔 에러가 나는 테스트는 `test.use({ expectedError: /.../ })` 로 그 에러만 허용한다.
- 회귀 케이스를 만들면 고친 코드를 잠시 되돌려 테스트가 실제로 실패하는지 확인한다. 통과만 확인한 테스트는 아무것도 지켜 주지 않을 수 있다.
- 날짜에 따라 결과가 달라지는 화면은 `page.clock` 으로 날짜를 고정한다. 시간대는 설정에서 `Asia/Seoul` 로 고정해 두었다.
28 changes: 28 additions & 0 deletions e2e/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 케이스 구현 전에 필요한 것

케이스 후보를 뽑으며 찾은, 테스트를 막는 지점이다. 해결하면 지운다.

## 마크업에 이름 붙이기

역할이나 이름이 없어 테스트가 사람처럼 찾을 수 없는 요소. 스크린 리더 사용자도 같은 이유로 쓰기 어렵다.
P1 케이스가 거치는 요소는 이름을 붙였다. 남은 것은 P2·P3 케이스용이다.

| 위치 | 문제 | 제안 |
| --- | --- | --- |
| src/pages/HomePage/components/TitleSection.tsx:56, 59 | ◀▶ 가 `div` onClick | `button` + `aria-label="이전"`/`"다음"` |
| 같은 파일 | 섹션 제목이 heading 이 아니라 "오늘"·"새로고침" 을 섹션별로 좁혀 잡을 수 없다 | `section aria-labelledby` 로 감싸기 |
| src/pages/HomePage/components/TaskReport/modals/TaskSettingModal.tsx:101 | 선택 버튼이 빈 `button` | `role="radio"` + `aria-checked` |
| src/pages/RoutinePage/.../EndDateField.tsx:31-35 | "추가"/"삭제" 가 다른 버튼과 이름이 겹침 | `aria-label="종료 날짜 추가"` |
| src/shared/ui/notices/components/NoticeList.tsx:27 | 공지 항목이 클릭만 되는 `div` | `button` |

## seed 확장

- `category` 의 `status: "ENDED"` 와 `endedAt`
- `routine` 의 `endDate`, 여러 구간의 `scheduleHistory`
- `task` 의 `time`
- `monthlyStats`: 마이 화면 달성률과 월간 통계 회귀 케이스용
- `notice`: 최상위 `notices` 컬렉션 (모든 사용자가 함께 본다)

## 확인이 필요한 앱 동작

- **다시 받아 온 집계가 진행 중인 체크 표시를 덮는다**: 체크하면 캘린더 숫자를 먼저 고쳐 두는데, 그사이 다른 곳(첫 문서 생성 뒤 재계산 등)이 월간 통계를 다시 받아 오면 고쳐 둔 값이 덮여 새로고침 전까지 옛 숫자가 보인다. 저장된 값은 맞다. 부하가 클 때 TC-HOME-004 에서 한 번 관찰했다. 체크가 끝났을 때 그사이 통계를 다시 받아 왔으면 한 번 더 받아 오는 식으로 막을 수 있으나, 재현을 고정하는 테스트를 먼저 만들어야 한다.
85 changes: 85 additions & 0 deletions e2e/cases/category.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# 카테고리

이 화면에는 삭제가 없다. 종료는 확인 창("카테고리를 종료할까요?")을 거친다.

### TC-CATEGORY-001 카테고리를 만들면 진행중 목록 맨 뒤에 나타난다
- 우선순위: P1 / 태그: @feature / 상태: 구현됨
- 전제: "건강"(orderIndex 0)
- 절차: "추가" → "공부" → 색 선택 → "저장" → 새로고침
- 기대: 건강, 공부 순서. 공부는 고른 색. 새로고침해도 같다
- 근거: src/shared/api/category/crud.ts:48-69

### TC-CATEGORY-002 가운데를 종료한 뒤 새로 만든 카테고리는 맨 뒤에 붙는다
- 우선순위: P2 / 태그: @regression / 상태: 제안
- 전제: A(0), B(1), C(2)
- 절차: B 종료(확인 창에서 "종료") → D 생성
- 기대: 진행중 목록이 A, C, D
- 근거: a8ddb5f, 5d1cd88

### TC-CATEGORY-003 순서 변경이 홈·루틴 화면에 반영된다
- 우선순위: P1 / 태그: @regression / 상태: 구현됨
- 전제: A, B, C 와 각각의 루틴. today 2026-10-21. 홈과 루틴을 한 번씩 열어 둔다
- 절차: C 를 맨 위로 끈다 → 저장 대기 → 루틴 → 홈
- 기대: 루틴 화면 묶음과 홈 주간 루틴 표가 모두 C, A, B
- 근거: 8474b19, src/shared/api/category/cache.ts:40-98

### TC-CATEGORY-004 이름·색을 바꾸면 루틴·기록·홈에 반영된다
- 우선순위: P1 / 태그: @feature / 상태: 구현됨
- 전제: "건강" 에 루틴 1개, 오늘 할 일 1개. 각 화면을 먼저 열어 둔다
- 절차: 건강 → "수정" → "운동", 색 변경 → 저장 → 루틴·기록·홈
- 기대: 세 화면 모두 "운동" 과 새 색. "건강" 은 어디에도 없다
- 근거: src/shared/api/category/invalidate.ts:22-33
- 기대(추가): 홈 "한 주" 보기의 할 일도 새 색으로 그려진다
- 발견: 이 케이스로 두 문제를 찾아 고쳤다. 홈 주간 루틴 표 캐시가 무효화되지 않아 옛 색이 남던 문제(invalidate.ts), 한 주 보기가 할 일 문서에 복사해 둔 옛 색을 영구히 그리던 문제(TaskWeekBoard.tsx)

### TC-CATEGORY-005 종료하면 종료된 카테고리로 옮겨지고 수정할 수 없다
- 우선순위: P1 / 태그: @feature / 상태: 구현됨
- 전제: A, B
- 절차: A → "종료" → 확인 창에서 "취소" → 다시 "종료" → 확인 창에서 "종료"
- 기대: 확인 창에 복구할 수 있다는 안내가 있다. 취소하면 상세 모달로 돌아가 그대로다. 종료하면 "종료된 카테고리" 에 A. A 상세에 "수정" 대신 "복구". 홈에서 A 에 할 일을 추가할 수 없다
- 근거: CategoryModal.tsx:188, 198-215

### TC-CATEGORY-006 복구하면 진행중 목록 맨 뒤로 돌아온다
- 우선순위: P2 / 태그: @feature / 상태: 제안
- 전제: 진행중 A, B. 종료된 C (seed 확장 필요)
- 기대: A, B, C. 종료 섹션이 비면 섹션째 사라진다
- 근거: crud.ts:119-134

### TC-CATEGORY-007 이름이 비었거나 공백뿐이면 저장되지 않는다
- 우선순위: P2 / 태그: @feature / 상태: 제안
- 기대: 공백만이면 "저장" 비활성, Enter 는 "카테고리 이름을 입력해 주세요." 21자는 20자까지만
- 근거: CategoryModal.tsx:26, 32-38, 101-106

### TC-CATEGORY-008 저장에 실패하면 토스트가 뜨고 모달이 남는다
- 우선순위: P2 / 태그: @feature / 상태: 제안
- 전제: 쓰기 실패
- 절차: 생성 / 수정 / 종료(확인 창에서 "종료")
- 기대: 각각의 실패 토스트, 모달 유지, 목록 그대로
- 근거: CategoryModal.tsx:85-99

### TC-CATEGORY-009 순서 저장에 실패하면 원래 순서로 돌아간다
- 우선순위: P2 / 태그: @feature / 상태: 제안
- 기대: "순서 저장에 실패했습니다…" 토스트, A, B, C 로 원복
- 근거: CategorySection.tsx:94-98

### TC-CATEGORY-010 저장을 연달아 눌러도 하나만 만들어진다
- 우선순위: P2 / 태그: @feature / 상태: 제안
- 전제: 쓰기 응답을 1초 늦춘다
- 기대: "저장 중...", 같은 이름이 1개만
- 근거: CategoryModal.tsx:67-68, 89

### TC-CATEGORY-011 수정 중에 닫으려 하면 확인을 묻는다
- 우선순위: P3 / 태그: @feature / 상태: 제안
- 기대: "수정 중인 내용이 있습니다". 공백만 덧붙였으면 묻지 않는다
- 근거: 3100b2c, CategoryModal.tsx:73-82

### TC-CATEGORY-012 첫 사용자에게 빈 화면 안내가 보인다
- 우선순위: P3 / 태그: @feature / 상태: 제안
- 기대: "진행중인 카테고리가 없습니다." 와 "카테고리 추가". "종료된 카테고리" 제목은 없다
- 근거: f801b9e, CategorySection.tsx:142-161

### TC-CATEGORY-013 목록을 읽지 못하면 다시 시도할 수 있다
- 우선순위: P3 / 태그: @feature / 상태: 제안
- 전제: 첫 조회만 실패
- 기대: "카테고리를 불러오지 못했습니다." → "다시 시도" 로 목록
- 근거: CategorySection.tsx:130-139
24 changes: 24 additions & 0 deletions e2e/cases/common.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 공통 (헤더·푸터·로그인 흐름)

### TC-COMMON-001 처음 로그인하면 프로필 문서가 자동으로 생긴다
- 우선순위: P1 / 태그: @regression / 상태: 구현됨
- 절차: 로그인만 하고 /home 이 뜨기를 기다린 뒤 관리자 권한으로 users/{uid} 를 읽는다
- 기대: 문서가 있고 uid, name 이 있으며 isAdmin 은 없다
- 근거: src/shared/stores/authStore.ts:95-101, firestore.rules:83-87, de153af

### TC-COMMON-002 로그인한 사용자가 / 나 /login 에 가면 /home 으로 간다
- 우선순위: P2 / 태그: @feature / 상태: 제안
- 기대: 두 경우 모두 /home, "달성 현황" 이 보인다
- 근거: src/pages/LandingPage/LandingPage.tsx:22, src/pages/LoginPage/LoginPage.tsx:19

### TC-COMMON-003 공지는 고정 공지가 먼저, 상세에서 이전·다음으로 오간다
- 우선순위: P2 / 태그: @feature / 상태: 제안
- 전제: 고정 1건, 일반 2건 (seed 확장 필요. 공지는 모든 사용자가 같이 보므로 제목에 고유값을 넣는다)
- 절차: 헤더 "공지" → 두 번째 공지 → "다음 공지" → 목록으로 → "닫기"
- 기대: 고정 공지 → 최신순. 상세 제목이 바뀌고 모달이 닫힌다
- 근거: src/shared/api/notice/queries.ts:14, src/shared/ui/notices/index.tsx:23-30

### TC-COMMON-004 푸터에서 약관으로 가고, 인스타그램은 새 창으로 열린다
- 우선순위: P3 / 태그: @feature / 상태: 제안
- 기대: /terms 로 이동. "checky 인스타그램 (새 창)" 은 target=_blank
- 근거: src/shared/ui/Footer.tsx:21-32, 10d7c86
Loading
Loading