Test: 네비게이션 검증 test 추가#205
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough여러 페이지에서 컨테이너를 div↔main으로 교체해 시맨틱 랜드마크를 정비했습니다. 홈 CTA는 Link 자체에 버튼 스타일을 적용했고, 네비게이션에 tabIndex 제어를 추가했습니다. 별 애니메이션에 보조 클래스가 추가되고 Playwright 테스트 인프라와 여러 E2E/성능 테스트가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant H as HomePage
participant L as Link(as Button)
participant R as Router
participant P as TargetPage
U->>H: 페이지 로드
H->>L: CTA Link 렌더링 (Link가 버튼 역할)
U->>L: Click / Enter
L-->>R: client-side navigation (href)
R-->>P: route transition
P-->>U: 새 페이지 렌더링
note right of L: 구조 변경: 내부 button 제거 → Link 자체가 인터랙티브 요소
sequenceDiagram
autonumber
actor U as Keyboard User
participant N as Navbar
participant M as MobileMenu
U->>N: Tab 키 입력
N-->>U: tabIndex=0 요소로 포커스 이동
alt 모바일 메뉴 닫힘
M-->>U: 메뉴 항목 tabIndex=-1 (포커스 불가)
else 모바일 메뉴 열림
M-->>U: 메뉴 항목 tabIndex=0 (포커스 가능)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
|
This pull request (commit
|
🚀 Storybook 배포📖 Storybook: https://683d91ab23651aa0b399e435-iwscwifygk.chromatic.com/ |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/(main)/setting/page.tsx (1)
45-76: 보안: window.open('_blank')에 noopener 미지정으로 reverse tabnabbing 위험이 있습니다.외부 링크를 새 탭으로 열 때는
noopener/noreferrer를 적용하거나opener를 명시적으로 끊어야 합니다.안전한 onClick 예시(현재 변경 구간 내에서 수정 가능):
- <SettingItem key={link.label} onClick={() => window.open(link.url, "_blank")}> + <SettingItem + key={link.label} + onClick={() => { + const w = window.open(link.url, "_blank", "noopener,noreferrer"); + if (w) w.opener = null; + }} + >대안(권장):
SettingItem이 앵커를 지원한다면<a target="_blank" rel="noopener noreferrer">로 렌더링하세요.
🧹 Nitpick comments (41)
app/(main)/my-capsule/page.tsx (1)
64-71: 메인 랜드마크 추가는 적절합니다. 다만 접근성 신호(aria-busy)와 스킵 링크용 id 추가를 권장합니다.무한 스크롤/초기 로딩 상태를 SR에 노출하려면 main에 aria-busy를 걸어두는 편이 좋습니다. 스킵 링크를 위해 id를 두는 것도 테스트와 a11y에 유용합니다.
아래처럼 main에 속성을 추가해 주세요:
- <main> + <main id="main-content" aria-busy={isPending || isFetchingNextPage}>추가 제안(선택):
- 로딩 스피너 영역에
role="status" aria-live="polite"를 부여하면 추가 페이지 로딩 시점이 SR에 안전하게 전달됩니다.- 페이지의 주 제목(TitleSection)이 실제로 문서의 메인 헤딩이라면 main 내부에 배치하는 것도 고려해 주세요(의미론 정합성 향상).
app/not-found.tsx (1)
12-24: 404 페이지의 main 적용은 적절합니다. h1 헤딩과 aria-labelledby 연결을 권장합니다.현재 첫 문구가 p 태그라 메인 영역의 대표 헤딩이 없습니다. SR·SEO 관점에서 h1을 두고 main과 연결하는 편이 좋습니다.
변경 예시(변경 라인 내에서 가능한 보완):
- <main className={styles.container}> + <main className={styles.container} aria-labelledby="notfound-title">변경 예시(참고: 변경 라인 외 보완 코드) — 대표 문구를 h1으로 교체하고 id를 부여:
<h1 id="notfound-title" className={styles.title}>페이지를 찾을 수 없습니다.</h1>추가 제안(선택):
NoneImage가 장식 목적이라면<NoneImage aria-hidden />로 스크린 리더 노이즈를 줄여주세요.app/(sub)/create-capsule/page.tsx (1)
115-137: 폼을 main으로 감싼 구조는 좋습니다. 제출 과정 노출을 위해 aria-busy를 main에 연결하세요.react-hook-form의
form.formState.isSubmitting을 활용해 제출 중 상태를 SR에 알리면 테스트 안정성과 접근성이 함께 좋아집니다.- <main className={styles.container}> + <main className={styles.container} aria-busy={form.formState.isSubmitting} id="create-capsule-main">app/(sub)/create-capsule/_components/steps/intro-step/index.tsx (1)
34-81: 중첩 main 제거는 잘 하셨습니다. 입력 요소에 레이블·설명 연결을 보완해 주세요.입력/텍스트영역이 placeholder만으로 노출되고 있어 SR 사용자에겐 레이블 맥락이 약합니다. 또한 글자수 카운터는
aria-describedby로 연결하면 접근성이 좋아집니다.보완 예시(참고 코드):
<label htmlFor="capsule-title" className="sr-only">타임캡슐 이름</label> <input id="capsule-title" type="text" placeholder="타임캡슐 이름" className={styles.titleInput} maxLength={15} aria-describedby="title-charcount" {...titleField} /> <span id="title-charcount" className={styles.charCountTitle} aria-live="polite"> {(titleField.value?.length || 0)}/15 </span> <label htmlFor="capsule-subtitle" className="sr-only">타임캡슐 소개</label> <textarea id="capsule-subtitle" placeholder="내 타임캡슐을 소개해보세요." className={styles.descriptionInput} maxLength={80} aria-describedby="subtitle-charcount" {...subtitleField} /> <span id="subtitle-charcount" className={styles.charCountDescription} aria-live="polite"> {(subtitleField.value?.length || 0)}/80 </span>app/(main)/setting/page.tsx (1)
45-76: main 적용은 적절합니다. 클릭 가능한 항목의 시맨틱/포커스 가능 여부를 확인해 주세요.
SettingItem이 div 기반이라면 role="button"과 키보드 핸들러(Enter/Space)를 제공하거나 a/button으로 렌더링하는 prop을 노출하는 것이 접근성 측면에서 바람직합니다.검토 포인트:
SettingItem이 a 혹은 button로 렌더링 가능한지(as prop 등)- div 클릭 시
tabIndex={0},role="button",onKeyDown(Enter/Space 트리거) 제공 여부app/(auth)/login/page.tsx (2)
40-41: 장식 이미지라면 스크린 리더 제외 권장.Lettie 캐릭터가 의미 전달이 아닌 장식이라면
alt=""로 비어두고(또는aria-hidden), 의미가 있다면 더 구체적인 대체 텍스트로 바꿔주세요.가능한 수정 예:
-<Image src={LettieImage} alt="Lettie" className={styles.lettieImage} /> +<Image src={LettieImage} alt="" aria-hidden className={styles.lettieImage} />
35-35: Skip 링크 연계를 위한 main id 부여 제안.E2E/접근성 테스트에서 “본문으로 건너뛰기” 링크를 추가할 여지를 위해
id="main-content"를 달아두면 좋습니다.-<main className={styles.contentsContainer}> +<main id="main-content" className={styles.contentsContainer}>app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx (1)
132-142: 메인 랜드마크 추가는 적절합니다만, 페이지 주요 제목(InfoTitle)을 main 내부로 포함 권장.스크린 리더 사용자에게 메인 영역의 ‘이름’을 제공하려면 주요 헤딩이 main 안에 있거나
aria-labelledby로 연결되는 편이 좋습니다. 현재 InfoTitle이 main 바깥에 있어 문맥 파악성이 다소 떨어질 수 있습니다.예시(간단 이동):
- <RevealMotion> - <InfoTitle - title={result.title} - participantCount={result.participantCount} - joinLettersCount={result.letterCount} - /> - </RevealMotion> - <main> + <main id="main-content"> + <RevealMotion> + <InfoTitle + title={result.title} + participantCount={result.participantCount} + joinLettersCount={result.letterCount} + /> + </RevealMotion> <CapsuleImage imageUrl={result.beadVideoUrl} />shared/ui/navbar/navbar-main/index.tsx (3)
32-53: a 태그에 부여된 tabIndex={0}는 중복이며 제거 권장.앵커(링크)는 기본적으로 포커스 가능하므로 불필요한
tabIndex={0}은 유지보수성만 떨어뜨립니다.-<Link href={PATH.HOME} tabIndex={0}> +<Link href={PATH.HOME}> ... -<Link href={PATH.EXPLORE} className={styles.buttonStyle} tabIndex={0}> +<Link href={PATH.EXPLORE} className={styles.buttonStyle}> ... -<Link href={PATH.MY_CAPSULE} className={styles.buttonStyle} tabIndex={0}> +<Link href={PATH.MY_CAPSULE} className={styles.buttonStyle}> ... -<Link href={PATH.SETTING} className={styles.buttonStyle} tabIndex={0}> +<Link href={PATH.SETTING} className={styles.buttonStyle}>
64-70: 모바일 메뉴 비가시 상태의 포커스/스크린 리더 차단 보강.
tabIndex={-1}만으로는 SR에서 읽힐 수 있습니다. 컨테이너에aria-hidden(또는inert)을 함께 적용해 주세요. 또한 버튼과aria-controls로 연결될 id를 부여해 주세요.-<ul +<ul className={cn( styles.menuContainer, isMenuOpen ? styles.menuContainerOpen : styles.navHomeStyleClose, )} + id="main-menu" + aria-hidden={!isMenuOpen} >추가로
nav에 명시적 이름을 줄 수 있으면 더 좋습니다:-<nav +<nav aria-label="메인 내비게이션" className={cn( styles.navHomeStyle, isMenuOpen ? styles.navHomeStyleOpen : styles.navHomeStyleClose, )} >Also applies to: 71-100
59-60: ARIA 속성 연결 및tabIndex제거 제안
HamburgerMenuButton은 네이티브<button>요소로 구현되어 있어tabIndex={0}은 불필요합니다.
또한 메뉴 열림 상태를 스크린 리더 등에 알리기 위해aria-expanded와aria-controls를 함께 전달하는 것을 권장합니다.• 대상 파일/위치
–shared/ui/navbar/navbar-main/index.tsx(59–60행)• 변경 사항
- 기존:
<HamburgerMenuButton isMenuOpen={isMenuOpen} onClick={handleMenuClick} tabIndex={0} />- 제안:
<HamburgerMenuButton isMenuOpen={isMenuOpen} onClick={handleMenuClick} - tabIndex={0} + aria-controls="main-menu" + aria-expanded={isMenuOpen} />이미
HamburgerMenuButton컴포넌트는...props로 전달받은 ARIA 속성을 내부<button>에 스프레드하므로, 위와 같이 간단히 추가 가능합니다.app/(main)/explore/page.tsx (1)
48-53: 주요 헤딩(TitleSection)을 main 내부로 이동 또는 연결 권장.현재
TitleSection이 main 바깥에 있어 SR 사용자 관점에서 메인 영역의 제목 연계가 약합니다. 컴포넌트 이동이 어렵다면main에aria-labelledby로 연결하는 방법도 있습니다.간단 이동 예:
- <RevealMotion> - <TitleSection /> - </RevealMotion> - <main> + <main id="main-content"> + <RevealMotion> + <TitleSection /> + </RevealMotion> <SelectTabSectionapp/(sub)/search/page.tsx (1)
41-41: Skip 링크 연계를 위한 main id 부여 제안.다른 페이지들과 일관성 있게
id="main-content"를 도입하면 좋습니다.-<main> +<main id="main-content">app/(main)/_components/home-button-section/index.tsx (1)
10-10: 불필요한 tabIndex 제거 제안앵커(
<a>)는 기본적으로 포커스 가능하므로tabIndex={0}은 중복입니다. 포커스 순서 혼선 방지를 위해 제거를 권장합니다.- <Link href={PATH.EXPLORE} className={styles.exploreButton} tabIndex={0}> + <Link href={PATH.EXPLORE} className={styles.exploreButton}>tests/navigation.spec.ts (2)
104-123: 키보드 내비게이션 테스트가 포커스 순서에 의존해 플래키합니다글로벌 네비/햄버거 등 포커스 가능한 요소가 늘어나면 Tab 횟수 기반 검증은 흔들립니다. 타깃 요소에 직접 포커스를 주고 Enter를 보내는 방식으로 안정화하세요.
- // Test tab navigation through interactive elements - await page.keyboard.press("Tab"); - - // Check if focus moves to the first interactive element - const focusedElement = page.locator(":focus"); - await expect(focusedElement).toBeVisible(); - - // Test tab navigation through multiple elements - await page.keyboard.press("Tab"); - - // Test Enter key on focused button - await page.keyboard.press("Enter"); + // 타깃 링크에 직접 포커스 후 Enter + const exploreLink = page.getByRole("link", { name: "타임캡슐 보러가기" }); + await exploreLink.focus(); + await expect(exploreLink).toBeFocused(); + await page.keyboard.press("Enter");
78-86: 직접 URL 내비게이션의 리다이렉트 판정이 타이밍 이슈에 취약합니다
page.goto("/my-capsule")직후page.url()을 체크하면 리다이렉트가 아직 완료되지 않았을 수 있습니다. URL 패턴 대기 또는 로드 상태 대기를 추가하세요.- await page.goto("/my-capsule"); - // This might redirect to login if not authenticated - const currentUrl = page.url(); + await page.goto("/my-capsule"); + // 이 단계에서 로그인 리다이렉트가 발생할 수 있으므로 URL 패턴을 대기 + await page.waitForURL(/\/(my-capsule|login)(\/|$)/); + const currentUrl = page.url();tests/run-tests.sh (2)
1-8: 스크립트 안정성을 위해 에러 옵션을 활성화하세요CI에서 조기 실패 감지를 위해
set -Eeuo pipefail추가를 권장합니다.#!/bin/bash - -# Playwright Test Runner Script +set -Eeuo pipefail + +# Playwright Test Runner Script
80-93: 테스트 파일 경로를 명시적으로 지정해 경로 해석 이슈를 방지하세요
pnpm test가 루트에서 실행될 때 파일만 넘기면 매칭 실패할 수 있습니다.tests/접두어를 붙여 안정화하세요.- run_tests "home.spec.ts" "" + run_tests "tests/home.spec.ts" "" @@ - run_tests "auth.spec.ts" "" + run_tests "tests/auth.spec.ts" "" @@ - run_tests "navigation.spec.ts" "" + run_tests "tests/navigation.spec.ts" "" @@ - run_tests "performance.spec.ts" "" + run_tests "tests/performance.spec.ts" "" @@ - run_tests "comprehensive.spec.ts" "" + run_tests "tests/comprehensive.spec.ts" ""tests/utils/test-helpers.ts (5)
44-55: 이미지 검증 순서를 조정해 실패 메시지를 명확히 하세요첫 요소 가시성 검증 전에 개수를 먼저 확인하면 “매칭 0건” 케이스에서 더 명확하게 실패합니다.
static async checkImagesLoaded(page: Page, selector: string) { const images = page.locator(selector); - await expect(images.first()).toBeVisible(); - - const imageCount = await images.count(); - expect(imageCount).toBeGreaterThan(0); + const imageCount = await images.count(); + expect(imageCount).toBeGreaterThan(0); + await expect(images.first()).toBeVisible();
57-67: 콘솔 리스너를 정리해 메모리 누수를 방지하세요테스트마다 핸들러가 누적되지 않도록 완료 후
page.off로 해제하세요.static async checkConsoleErrors(page: Page) { const consoleErrors: string[] = []; - page.on("console", (msg) => { + const handler = (msg: any) => { if (msg.type() === "error") { consoleErrors.push(msg.text()); } - }); + }; + page.on("console", handler); await page.waitForLoadState("networkidle"); + page.off("console", handler); return consoleErrors; }
69-77: 요청 실패 리스너도 동일하게 해제하세요완료 후
page.off("requestfailed", handler)호출을 추가하세요.static async checkFailedRequests(page: Page) { const failedRequests: string[] = []; - page.on("requestfailed", (request) => { + const handler = (request: any) => { failedRequests.push(request.url()); - }); + }; + page.on("requestfailed", handler); await page.waitForLoadState("networkidle"); + page.off("requestfailed", handler); return failedRequests; }
79-83: 느린 네트워크 시뮬레이션에 정리(cleanup)를 제공하면 테스트 간 간섭을 줄일 수 있습니다현재 라우트는 명시적으로 해제되지 않습니다. 호출부에서 해제할 수 있도록 클린업을 반환하는 유틸 대안을 제안합니다.
아래는 새로운 헬퍼 예시입니다(외부 변경을 돕기 위한 추가 코드 블록):
// 추가 유틸 (새 메서드로 제안) static async withSlowNetwork<T>(page: Page, delayMs = 1000, fn: () => Promise<T>) { const routeGlob = "**/*"; await page.route(routeGlob, (route) => setTimeout(() => route.continue(), delayMs)); try { return await fn(); } finally { await page.unroute(routeGlob); } }
98-110: 숫자 텍스트 폴링이 광범위합니다 — 선택자/범위를 좁히는 것을 고려하세요
text=/\d+/는 페이지 전역 숫자 모두에 매칭됩니다.data-testid기반 특정 영역 한정 또는 더 구체적인 셀렉터를 받는 시그니처로 확장하는 것을 권장합니다.tests/performance.spec.ts (2)
14-16: 성능 임계값을 환경변수로 조절 가능하게 하여 플래키를 줄이세요CI 환경 변동을 고려해 상수(3000ms) 대신 환경변수 기반으로 완화하세요(예: 기본 5000ms).
- // Home page should load within 3 seconds - expect(loadTime).toBeLessThan(3000); + const maxMs = Number(process.env.PERF_HOME_MAX_MS ?? "5000"); + // Home page should load within configurable threshold + expect(loadTime).toBeLessThan(maxMs);
75-79: 페이지 전환 시간 임계값도 설정 가능하도록 통일하세요전환 1초 기준 역시 환경변수로 조정 가능하게 두면 환경차로 인한 flaky를 줄일 수 있습니다.
- // Navigation should be fast (less than 1 second) - expect(navigationTime).toBeLessThan(1000); + const maxNavMs = Number(process.env.PERF_NAV_MAX_MS ?? "1500"); + // Navigation should be fast + expect(navigationTime).toBeLessThan(maxNavMs);shared/ui/floating-stars-container/index.tsx (4)
63-63: 테스트 안정성과 접근성 향상: data-testid/aria-hidden 추가를 권장합니다E2E 테스트가 클래스명에 결합되지 않도록 안정적인 선택자(data-testid)를 노출하고, 순수 장식 요소임을 명시(스크린리더 제외)하면 접근성과 유지보수성이 좋아집니다. 필요 시 포인터 차단으로 상호작용 영역을 보호할 수 있습니다.
아래처럼 수정을 제안합니다:
- <div className={`${styles.container} floating-stars`}> + <div + className={`${styles.container} floating-stars`} + data-testid="floating-stars" + aria-hidden="true" + style={{ pointerEvents: "none" }} + >
35-36: any[] 지양: Star 타입 명시로 가독성과 안전성 개선
useState<any[]>는 타입 안정성을 해칩니다. 내부 구조가 고정되어 있으므로 명시적 Star 인터페이스를 두고 제네릭을 지정해주세요.예시(파일 상단 인접 위치에 추가):
interface Star { id: string; left: string; speed: number; size: number; sway: number; startPosition: number; swayOffset: number; }- const [stars, setStars] = useState<any[]>([]); + const [stars, setStars] = useState<Star[]>([]);
42-44: 속도 0/음수 방지: duration 분모 보호(clamp) 필요
duration: 10 / star.speed는speed가 0 또는 매우 작을 때 런타임 오류/과도한 duration을 유발할 수 있습니다. star.speed 산출 시 하한선을 두세요.- speed: speed * (1 + (seededRandom(seed + 1) - 0.5) * 2 * speedRandomness), + speed: Math.max( + 0.1, + speed * (1 + (seededRandom(seed + 1) - 0.5) * 2 * speedRandomness) + ),Also applies to: 81-87
74-79: 단위 혼용(px ↔ %)으로 인한 애니메이션 불안정 가능성초기 스타일은
bottom: -{size}px인데 애니메이션은bottom: ["0%", "100%"]로 단위가 다릅니다. 브라우저/모션 엔진에 따라 경고/깜빡임이 생길 수 있습니다. 하나의 단위로 통일하거나initial프롭을 사용해 동일 단위로 시작/종료를 정의해주세요.예시(단위 통일):
- style={{ ... , bottom: `${-star.size}px` }} + style={{ ... , bottom: "0%" }} - animate={{ bottom: ["0%", "100%"], ... }} + animate={{ bottom: ["0%", "100%"], ... }}또는 transform 기반 이동(
y)으로 전환하여 레이아웃 영향 최소화도 고려해볼 만합니다.Also applies to: 77-79
tests/README.md (3)
7-17: 마크다운 린트(MD040) 해결: 코드 블록에 언어 지정파일 트리 코드 블록에 언어가 없습니다.
markdownlint가 경고합니다.text등을 명시해주세요.-``` +```text tests/ ├── README.md # 이 파일 ... -``` +```
114-121: 웹서버 커맨드/모드 명확화: 로컬과 CI 분리Playwright
webServer를pnpm run start로 고정하면 로컬에서 빌드 필요 등 번거로움이 있습니다. 로컬은dev, CI는start로 분기하거나, 빌드/스타트를 config 또는 CI에서 명시하세요.예시:
// playwright.config.ts const isCI = !!process.env.CI; webServer: { command: isCI ? "pnpm run start" : "pnpm run dev", url: "http://localhost:3000", reuseExistingServer: !isCI, }
126-142: 쿠키 기반 인증 가이드 보완: addCookies 시점/속성 주의문서에 쿠키 주입 예시를 추가할 때, 반드시
page.goto이전에context.addCookies를 호출하고,localhost는domain대신url을 쓰는 것을 권장합니다(호스트 전용 쿠키 호환성).예시:
await page.context().addCookies([{ name: "accessToken", value: "TEST_TOKEN", url: "http://localhost:3000", path: "/", httpOnly: true, secure: false, }]); await page.goto("/");또는
storageState를 활용해 전역 설정:// playwright.config.ts testDir: "./tests", use: { storageState: "tests/.auth/state.json" }tests/home.spec.ts (3)
27-33: 역할(Role) 일치: CTA가 Link라면 button 대신 link를 사용PR 요약에 따르면 CTA는 Link 스타일입니다. 실제 DOM 역할이 anchor(link)라면
getByRole("button")은 취약합니다.getByRole("link")로 교체하고 클릭/네비게이션은 동일하게 동작합니다.- page.getByRole("button", { name: "타임캡슐 보러가기" }) + page.getByRole("link", { name: "타임캡슐 보러가기" }) - page.getByRole("button", { name: "타임캡슐 만들기" }) + page.getByRole("link", { name: "타임캡슐 만들기" })Also applies to: 41-49, 54-60
63-69: 고정 대기 제거: 폴링/조건 대기 적용으로 플레이크 감소
waitForTimeout(2000)은 환경에 따라 불안정합니다. 조건 기반 대기 또는 헬퍼를 사용하세요.간단 폴링:
- await page.waitForTimeout(2000); - const userCountElement = page.locator("text=/\\d+/").first(); - await expect(userCountElement).toBeVisible(); + await expect(page.locator("text=/\\d+/").first()).toBeVisible({ timeout: 10000 });또는 TestHelpers 활용:
import { TestHelpers } from "./utils/test-helpers"; expect(await TestHelpers.waitForUserCountToLoad(page, 10000)).toBe(true);
81-84: 클래스 기반 선택자 최소화: floating-stars도 testId 사용앞서 UI에
data-testid="floating-stars"를 추가했다면 테스트도 이를 사용해주세요.- await expect(page.locator("[class*='floating-stars']")).toBeVisible(); + await expect(page.getByTestId("floating-stars")).toBeVisible();tests/comprehensive.spec.ts (6)
5-17: 쿠키 속성 정비: localhost 호환성 향상을 위해 url 사용
domain: "localhost"는 브라우저/헬퍼 구현에 따라 호환성 이슈가 있을 수 있습니다.url을 사용해 호스트 전용 쿠키로 설정하는 편이 안전합니다.- { - name: "accessToken", - value: "TEST_TOKEN", - domain: "localhost", - path: "/", - httpOnly: true, - secure: false, - }, + { + name: "accessToken", + value: "TEST_TOKEN", + url: "http://localhost:3000", + path: "/", + httpOnly: true, + secure: false, + },
50-69: 성능 임계값 완화/환경 분기: CI에서 과도하게 빡빡할 수 있음홈 로드 < 3초, 탐색 네비 < 1초 조건은 CI(공유 머신)에서 플레이크를 유발합니다. CI 여부에 따라 임계값을 분기하거나, 측정 구간을 축소(URL 변경까지만 측정)하세요.
예시(임계값 분기):
- expect(loadTime).toBeLessThan(3000); + expect(loadTime).toBeLessThan(process.env.CI ? 5000 : 3000); ... - expect(navigationTime).toBeLessThan(1000); + expect(navigationTime).toBeLessThan(process.env.CI ? 3000 : 1000);또는 네비 성능은
waitForURL까지만 시간 측정:const t0 = Date.now(); await page.getByRole("link", { name: "타임캡슐 보러가기" }).click(); await page.waitForURL("**/explore"); const t = Date.now() - t0;
71-84: 콘솔/네트워크 에러 감시 시점 조정현재는 홈 이동 후 리스너를 등록하므로 초기 로드 중 발생한 에러를 놓칠 수 있습니다. 리스너를 먼저 등록한 뒤 네비게이션/로딩을 수행하세요.
예시:
const consoleErrors = await TestHelpers.checkConsoleErrors(page); const failedRequests = await TestHelpers.checkFailedRequests(page); await TestHelpers.navigateToHome(page); expect(consoleErrors.length).toBe(0); expect(failedRequests.length).toBe(0);
91-102: 부하 테스트 안정화: 반복 횟수/타임아웃 조정5회 왕복은 로컬에선 OK여도 CI에서 느려질 수 있습니다.
test.slow()지정 또는 CI에서 반복 횟수 축소를 고려해주세요.test.slow(); const loops = process.env.CI ? 3 : 5; for (let i = 0; i < loops; i++) { ... }
104-111: 키보드 내비게이션 검증 강화Tab 1회 후
:focus가시성만으로는 충분치 않습니다. 포커스가 의도한 첫 포커스 가능한 요소(예: 주요 CTA/네비)에 도달했는지까지 확인하면 신뢰도가 올라갑니다.예시:
await page.keyboard.press("Tab"); await expect(page.getByRole("link", { name: "타임캡슐 보러가기" })).toBeFocused();
129-146: API 콜 검증 정밀화: 엔드포인트/상태코드 기준으로 대기
url.includes("user")는 모호합니다. 구체 엔드포인트와 2xx 응답을 기준으로waitForResponse를 사용하면 플레이크를 줄일 수 있습니다.- const userCountApiCall = apiCalls.find((url) => url.includes("user")); - expect(userCountApiCall).toBeTruthy(); + const resp = await page.waitForResponse((r) => + r.url().includes("/api/users") && r.status() >= 200 && r.status() < 300 + , { timeout: 10000 }); + expect(resp.ok()).toBeTruthy();
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (20)
app/(auth)/login/page.tsx(2 hunks)app/(main)/_components/home-button-section/index.tsx(1 hunks)app/(main)/explore/page.tsx(1 hunks)app/(main)/my-capsule/page.tsx(1 hunks)app/(main)/setting/page.tsx(2 hunks)app/(sub)/capsule-detail/[invite-code]/[id]/page.tsx(2 hunks)app/(sub)/create-capsule/_components/steps/intro-step/index.tsx(2 hunks)app/(sub)/create-capsule/page.tsx(2 hunks)app/(sub)/search/page.tsx(1 hunks)app/not-found.tsx(2 hunks)shared/ui/floating-stars-container/index.tsx(1 hunks)shared/ui/navbar/navbar-main/index.tsx(4 hunks)tests/README.md(1 hunks)tests/comprehensive.spec.ts(1 hunks)tests/example.spec.ts(0 hunks)tests/home.spec.ts(1 hunks)tests/navigation.spec.ts(1 hunks)tests/performance.spec.ts(1 hunks)tests/run-tests.sh(1 hunks)tests/utils/test-helpers.ts(1 hunks)
💤 Files with no reviewable changes (1)
- tests/example.spec.ts
🧰 Additional context used
🧬 Code graph analysis (5)
tests/performance.spec.ts (1)
app/(main)/_components/home-button-section/home-button-section.css.ts (1)
exploreButton(43-52)
tests/comprehensive.spec.ts (1)
tests/utils/test-helpers.ts (1)
TestHelpers(3-140)
shared/ui/navbar/navbar-main/index.tsx (1)
shared/constants/path.ts (1)
PATH(1-13)
tests/home.spec.ts (1)
app/(main)/_components/home-button-section/home-button-section.css.ts (1)
exploreButton(43-52)
app/(main)/_components/home-button-section/index.tsx (1)
shared/constants/path.ts (1)
PATH(1-13)
🪛 markdownlint-cli2 (0.17.2)
tests/README.md
7-7: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: storybook-deploy
- GitHub Check: test
🔇 Additional comments (6)
app/(auth)/login/page.tsx (2)
35-61: 메인 랜드마크 도입: 접근성 측면에서 적절합니다 (LGTM).헤더와 본문을 명확히 분리했고, 테스트 안정화(랜드마크 탐색)에도 유리합니다.
21-26: 내부 경로 검증 로직 확인 완료
shared/utils/oauth.ts의saveNextUrl구현을 보면nextUrl?.startsWith("/") && !nextUrl.startsWith("//")조건으로 오직 “/”로 시작하고 “//”로 시작하지 않는 상대경로만 sessionStorage에 저장하도록 되어 있습니다.
외부 도메인이나 프로토콜·호스트가 포함된 값은 저장되지 않아, 오픈 리다이렉트 위험이 없음을 확인했습니다.
추가 검증 로직이나 화이트리스트 도입 없이도 안전하므로, 해당 리뷰는 해결된 것으로 보입니다.app/(main)/explore/page.tsx (1)
51-59: 메인 랜드마크 추가: 전반적으로 적절합니다.무한 스크롤/카드 목록이 main 아래로 정리되어 탐색이 수월합니다.
app/(sub)/search/page.tsx (1)
41-44: 메인 랜드마크 추가: 검색 UX에 적합합니다.검색바와 결과 목록이 main 하위로 묶여 있어 테스트 및 키보드 탐색이 쉬워졌습니다.
app/(main)/_components/home-button-section/index.tsx (1)
7-24: Link 기반 CTA 전환은 접근성/시맨틱 측면에서 적절합니다네비게이션 목적의 CTA를 링크로 통일하고, 시맨틱 구조를 보존한 채 스타일링하는 접근이 좋습니다.
tests/README.md (1)
21-31: 테스트 실행 스크립트 확인 완료package.json의
scripts.test가 이미playwright test로 정의되어 있어, 문서에 안내된pnpm test명령이 올바르게 Playwright를 호출합니다. 별도 수정이 필요 없습니다.
| <Link href={PATH.EXPLORE} className={styles.exploreButton} tabIndex={0}> | ||
| 타임캡슐 보러가기 | ||
| <div className={styles.lineInteractContainer}> | ||
| <div className={styles.lineInteract}></div> | ||
| </div> | ||
| </Link> | ||
|
|
||
| <Link href={PATH.CREATE_CAPSULE}> | ||
| <button type="button" className={styles.button}> | ||
| 타임캡슐 만들기 | ||
| </button> | ||
| <Link href={PATH.CREATE_CAPSULE} className={styles.button}> | ||
| 타임캡슐 만들기 | ||
| </Link> |
There was a problem hiding this comment.
CTA를 Link로 바꾼 뒤 테스트 쿼리(role=button)와 불일치합니다
현재 CTA는 Next.js <Link>로 변경되어 ARIA role이 "link"입니다. 반면 본 PR에 포함된 e2e/헬퍼는 "button" 역할로 쿼리하고 있어 실패합니다. 네비게이션 목적의 요소는 링크가 맞으니 컴포넌트를 다시 버튼으로 되돌리기보다는 테스트/헬퍼 쪽을 "link"로 정정하는 것을 권장합니다. 관련 수정 제안을 tests/navigation.spec.ts 및 tests/utils/test-helpers.ts 코멘트에 첨부했습니다.
| <Link href={PATH.SEARCH} className={styles.searchButtonStyle} tabIndex={0}> | ||
| <SearchIcon width={"2rem"} height={"2rem"} /> | ||
| </Link> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
아이콘 전용 검색 링크에 접근 가능한 이름 추가 필수.
시각적 텍스트가 없으므로 스크린 리더에 ‘검색’으로 읽히도록 aria-label을 부여해주세요.
-<Link href={PATH.SEARCH} className={styles.searchButtonStyle} tabIndex={0}>
+<Link href={PATH.SEARCH} className={styles.searchButtonStyle} aria-label="검색">
<SearchIcon width={"2rem"} height={"2rem"} />
</Link>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Link href={PATH.SEARCH} className={styles.searchButtonStyle} tabIndex={0}> | |
| <SearchIcon width={"2rem"} height={"2rem"} /> | |
| </Link> | |
| <Link href={PATH.SEARCH} className={styles.searchButtonStyle} aria-label="검색"> | |
| <SearchIcon width={"2rem"} height={"2rem"} /> | |
| </Link> |
🤖 Prompt for AI Agents
shared/ui/navbar/navbar-main/index.tsx lines 53-55: the icon-only search link
lacks an accessible name for screen readers; add an aria-label (e.g.,
aria-label="검색") to the Link element so assistive tech reads "검색", keep existing
tabIndex, and ensure the label is localized if needed.
| await page.goto("/"); | ||
| // 테스트 시작 전에 인증 쿠키 추가 | ||
| await page.context().addCookies([ | ||
| { | ||
| name: "accessToken", | ||
| value: "TEST_TOKEN", | ||
| domain: "localhost", | ||
| path: "/", | ||
| httpOnly: true, | ||
| secure: false, | ||
| }, | ||
| ]); | ||
| }); |
There was a problem hiding this comment.
인증 쿠키 주입 시점 수정: goto 이전 + domain 대신 url 사용
현재 page.goto("/") 이후에 쿠키를 추가하고 있어 첫 요청이 비인증 상태로 진행됩니다. 또한 localhost에는 domain보다 url 지정이 호환성에 안전합니다. 아래처럼 순서를 바꾸고 속성을 정정해주세요.
- test.beforeEach(async ({ page }) => {
- await page.goto("/");
- // 테스트 시작 전에 인증 쿠키 추가
- await page.context().addCookies([
- {
- name: "accessToken",
- value: "TEST_TOKEN",
- domain: "localhost",
- path: "/",
- httpOnly: true,
- secure: false,
- },
- ]);
- });
+ test.beforeEach(async ({ page }) => {
+ await page.context().addCookies([
+ {
+ name: "accessToken",
+ value: "TEST_TOKEN",
+ url: "http://localhost:3000",
+ path: "/",
+ httpOnly: true,
+ secure: false,
+ },
+ ]);
+ await page.goto("/");
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await page.goto("/"); | |
| // 테스트 시작 전에 인증 쿠키 추가 | |
| await page.context().addCookies([ | |
| { | |
| name: "accessToken", | |
| value: "TEST_TOKEN", | |
| domain: "localhost", | |
| path: "/", | |
| httpOnly: true, | |
| secure: false, | |
| }, | |
| ]); | |
| }); | |
| test.beforeEach(async ({ page }) => { | |
| await page.context().addCookies([ | |
| { | |
| name: "accessToken", | |
| value: "TEST_TOKEN", | |
| url: "http://localhost:3000", | |
| path: "/", | |
| httpOnly: true, | |
| secure: false, | |
| }, | |
| ]); | |
| await page.goto("/"); | |
| }); |
🤖 Prompt for AI Agents
In tests/home.spec.ts around lines 5 to 17, the cookie is added after page.goto
and uses domain which can be incompatible; move the page.context().addCookies
call to before page.goto and replace the cookie's domain field with a url field
(e.g., "http://localhost/") while keeping name, value, path and other flags
intact so the initial request is sent with the injected auth cookie.
| await expect(page.locator("[class*='physicsContainer']")).toBeVisible(); | ||
| }); |
There was a problem hiding this comment.
해시된 클래스 의존 최소화: data-testid로 교체 권장
[class*='physicsContainer']는 vanilla-extract/빌드 설정에 따라 프로덕션에서 식별자가 소실될 수 있어 매우 취약합니다. 안정적인 data-testid="physics-container"를 노출하고 테스트를 해당 testId로 전환하세요.
- await expect(page.locator("[class*='physicsContainer']")).toBeVisible();
+ await expect(page.getByTestId("physics-container")).toBeVisible();UI 측(물리 컨테이너 루트)에 다음 속성 추가를 권장합니다(별도 PR/커밋 가능):
<div data-testid="physics-container"> ... </div>🤖 Prompt for AI Agents
In tests/home.spec.ts around lines 35-36, the test is relying on a
hashed/fragile CSS selector "[class*='physicsContainer']" which can break in
production; update the test to use a stable test id by selecting
page.locator('[data-testid="physics-container"]') instead, and coordinate a UI
change (add data-testid="physics-container" to the physics container root
element) if not already present so the test targets the stable attribute.
| test.beforeEach(async ({ page }) => { | ||
| await page.goto("/"); | ||
| // 테스트 시작 전에 인증 쿠키 추가 | ||
| await page.context().addCookies([ | ||
| { | ||
| name: "accessToken", | ||
| value: "TEST_TOKEN", | ||
| domain: "localhost", | ||
| path: "/", | ||
| httpOnly: true, | ||
| secure: false, | ||
| }, | ||
| ]); | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
인증 쿠키 추가 시점이 늦습니다 — SSR/리다이렉트 일관성을 위해 쿠키를 먼저 설정하세요
await page.goto("/") 이후에 쿠키를 넣으면 첫 페이지 로딩(SSR/미들웨어) 시 인증이 반영되지 않아 플래키할 수 있습니다. 쿠키 설정을 먼저 하고 이후 goto 하도록 순서를 바꾸세요.
test.beforeEach(async ({ page }) => {
- await page.goto("/");
- // 테스트 시작 전에 인증 쿠키 추가
- await page.context().addCookies([
+ // 테스트 시작 전에 인증 쿠키 추가
+ await page.context().addCookies([
{
name: "accessToken",
value: "TEST_TOKEN",
domain: "localhost",
path: "/",
httpOnly: true,
secure: false,
},
]);
+ await page.goto("/");
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test.beforeEach(async ({ page }) => { | |
| await page.goto("/"); | |
| // 테스트 시작 전에 인증 쿠키 추가 | |
| await page.context().addCookies([ | |
| { | |
| name: "accessToken", | |
| value: "TEST_TOKEN", | |
| domain: "localhost", | |
| path: "/", | |
| httpOnly: true, | |
| secure: false, | |
| }, | |
| ]); | |
| }); | |
| test.beforeEach(async ({ page }) => { | |
| // 테스트 시작 전에 인증 쿠키 추가 | |
| await page.context().addCookies([ | |
| { | |
| name: "accessToken", | |
| value: "TEST_TOKEN", | |
| domain: "localhost", | |
| path: "/", | |
| httpOnly: true, | |
| secure: false, | |
| }, | |
| ]); | |
| await page.goto("/"); | |
| }); |
🤖 Prompt for AI Agents
In tests/navigation.spec.ts around lines 4–17, the test sets the authentication
cookie after calling page.goto, which means the first request (SSR/middleware)
won’t see the cookie and can cause flakiness; move the await
page.context().addCookies([...]) call to run before await page.goto("/") so the
cookie is present for the initial navigation (optionally ensure a fresh context
by clearing existing cookies before adding if needed).
| await page.goto("/"); | ||
|
|
||
| // Check for any console errors | ||
| const consoleErrors: string[] = []; | ||
| page.on("console", (msg) => { | ||
| if (msg.type() === "error") { | ||
| consoleErrors.push(msg.text()); | ||
| } | ||
| }); | ||
|
|
||
| await page.waitForLoadState("networkidle"); | ||
|
|
||
| // Should not have any console errors | ||
| expect(consoleErrors.length).toBe(0); | ||
|
|
||
| // Check for any failed requests | ||
| const failedRequests: string[] = []; | ||
| page.on("requestfailed", (request) => { | ||
| failedRequests.push(request.url()); | ||
| }); | ||
|
|
||
| // Should not have any failed requests | ||
| expect(failedRequests.length).toBe(0); | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
초기 콘솔/네트워크 오류를 놓치지 않도록 핸들러 등록 순서를 바꾸세요
오류/실패한 요청 리스너를 goto("/") 이전에 등록해야 초기 로드 중 발생한 이슈를 포착할 수 있습니다.
- test("should optimize bundle size and loading", async ({ page }) => {
- await page.goto("/");
-
- // Check for any console errors
- const consoleErrors: string[] = [];
- page.on("console", (msg) => {
+ test("should optimize bundle size and loading", async ({ page }) => {
+ // 등록을 먼저 해서 초기 로드 이슈 포착
+ const consoleErrors: string[] = [];
+ page.on("console", (msg) => {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});
-
- await page.waitForLoadState("networkidle");
-
- // Should not have any console errors
- expect(consoleErrors.length).toBe(0);
-
- // Check for any failed requests
- const failedRequests: string[] = [];
- page.on("requestfailed", (request) => {
+ const failedRequests: string[] = [];
+ page.on("requestfailed", (request) => {
failedRequests.push(request.url());
});
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+
+ // Should not have any console errors
+ expect(consoleErrors.length).toBe(0);
// Should not have any failed requests
expect(failedRequests.length).toBe(0);
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await page.goto("/"); | |
| // Check for any console errors | |
| const consoleErrors: string[] = []; | |
| page.on("console", (msg) => { | |
| if (msg.type() === "error") { | |
| consoleErrors.push(msg.text()); | |
| } | |
| }); | |
| await page.waitForLoadState("networkidle"); | |
| // Should not have any console errors | |
| expect(consoleErrors.length).toBe(0); | |
| // Check for any failed requests | |
| const failedRequests: string[] = []; | |
| page.on("requestfailed", (request) => { | |
| failedRequests.push(request.url()); | |
| }); | |
| // Should not have any failed requests | |
| expect(failedRequests.length).toBe(0); | |
| }); | |
| test("should optimize bundle size and loading", async ({ page }) => { | |
| // 등록을 먼저 해서 초기 로드 이슈 포착 | |
| const consoleErrors: string[] = []; | |
| page.on("console", (msg) => { | |
| if (msg.type() === "error") { | |
| consoleErrors.push(msg.text()); | |
| } | |
| }); | |
| const failedRequests: string[] = []; | |
| page.on("requestfailed", (request) => { | |
| failedRequests.push(request.url()); | |
| }); | |
| await page.goto("/"); | |
| await page.waitForLoadState("networkidle"); | |
| // Should not have any console errors | |
| expect(consoleErrors.length).toBe(0); | |
| // Should not have any failed requests | |
| expect(failedRequests.length).toBe(0); | |
| }); |
🤖 Prompt for AI Agents
In tests/performance.spec.ts around lines 103 to 126, the console and
requestfailed event handlers are registered after page.goto("/"), so errors and
failed requests that occur during initial navigation are missed; move the
page.on("console", ...) and page.on("requestfailed", ...) registrations so they
execute before calling page.goto("/"), then perform
page.waitForLoadState("networkidle") and the expect assertions after navigation
completes to capture any initial load errors or failed requests.
|
This pull request (commit
|
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
tests/home.spec.ts (2)
35-36: 해시 클래스 의존 제거: 안정적인 test id로 전환빌드 후 클래스명이 바뀌면 테스트가 깨집니다.
data-testid로 교체 권장.- await expect(page.locator("[class*='physicsContainer']")).toBeVisible(); + await expect(page.getByTestId("physics-container")).toBeVisible();UI에도 루트에
data-testid="physics-container"를 노출해 주세요.
4-17: 인증 쿠키 주입 시점/속성 수정: goto 이전 + url 사용첫 요청이 비인증 상태로 나가며,
domain대신url을 사용해야 로컬 환경에서 호환성이 높습니다. 아래처럼 수정해 주세요.test.beforeEach(async ({ page }) => { - await page.goto("/"); - // 테스트 시작 전에 인증 쿠키 추가 - await page.context().addCookies([ + // 테스트 시작 전에 인증 쿠키 추가 + await page.context().addCookies([ { name: "accessToken", value: "TEST_TOKEN", - domain: "localhost", + url: "http://localhost:3000", path: "/", httpOnly: true, secure: false, }, ]); + await page.goto("/"); });추가로, 중복 설정 방지를 위해 storageState(globalSetup) 기반 인증 고정도 고려해 주세요. 필요 시 설정 템플릿 제공 가능합니다.
🧹 Nitpick comments (11)
tests/README.md (4)
7-17: 코드 블록 언어 지정 누락(MD040) — language 지정으로 lint 에러 제거디렉토리 트리 예시는 언어 지정을 안 해서 markdownlint(MD040)에 걸립니다.
text등으로 지정해주세요.-``` +```text tests/ ├── README.md # 이 파일 ├── utils/ │ └── test-helpers.ts # 공통 테스트 유틸리티 ├── home.spec.ts # 홈페이지 기능 테스트 ├── auth.spec.ts # 인증 기능 테스트 ├── navigation.spec.ts # 네비게이션 테스트 ├── performance.spec.ts # 성능 및 로딩 테스트 ├── comprehensive.spec.ts # 종합 테스트 스위트--- `26-31`: **실행 예시에 누락된 스펙 추가** 아래 섹션의 단일 파일 실행 예시에 `performance.spec.ts`, `comprehensive.spec.ts`도 포함해 주세요. 사용자가 README만 보고도 전체 스펙을 쉽게 실행할 수 있도록 합니다. ```diff pnpm test home.spec.ts pnpm test auth.spec.ts pnpm test navigation.spec.ts +pnpm test performance.spec.ts +pnpm test comprehensive.spec.ts
114-117: CI 환경 설정 예시를 코드로 명시 — 재현성 향상문장 설명만으로는 설정이 모호합니다.
retries/workers를 CI에서 강제하는 예시 코드를 추가해 주세요.- `CI`: CI 환경에서만 재시도 및 제한된 워커 사용 - `CI` 환경에서는 2회 재시도, 1개 워커 사용 + +예시 설정: +```ts +// playwright.config.ts (발췌) +export default defineConfig({ + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, +}); +```
195-214: GitHub Actions 안정성 개선: pnpm 캐시 + with-deps 설치CI에서 WebKit 등 시스템 의존 패키지가 필요합니다.
--with-deps사용과 pnpm 캐시를 추가하면 플레이크를 줄이고 속도를 개선할 수 있습니다. Node 액션도 최신 메이저로 교체 권장.name: Playwright Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: 18 - - run: pnpm install - - run: pnpm exec playwright install + cache: 'pnpm' + - uses: pnpm/action-setup@v4 + with: + version: 9 + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps - run: pnpm test - uses: actions/upload-artifact@v3 if: always() with: name: playwright-report path: playwright-report/tests/home.spec.ts (7)
1-1: 공통 유틸 사용으로 중복 제거이미
tests/utils/test-helpers.ts가 존재하므로 본 스펙도 헬퍼를 활용해 유지보수성을 높이면 좋습니다. 예: 반응형/로드 대기/에러 체크 등.-import { expect, test } from "@playwright/test"; +import { expect, test } from "@playwright/test"; +import { TestHelpers } from "./utils/test-helpers";
23-25: 시맨틱/접근성 우선 셀렉터 사용
h1태그 직접 선택 대신 Role 기반이 내구성이 높습니다.- await expect(page.locator("h1")).toBeVisible(); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
46-49: 중복 단언 축소: toHaveURL 한 줄로 대체
waitForURL+page.url()이중 검증은冗長합니다.- // Wait for navigation - await page.waitForURL("**/explore"); - expect(page.url()).toContain("/explore"); + await expect(page).toHaveURL(/\/explore(?:$|\?)/);
57-60: URL 검증 일관화위와 동일하게
toHaveURL사용으로 간결화하세요.- // Wait for navigation - await page.waitForURL("**/create-capsule"); - expect(page.url()).toContain("/create-capsule"); + await expect(page).toHaveURL(/\/create-capsule(?:$|\?)/);
71-79: 접근성 친화적 선택자 사용 또는 헬퍼 위임
img[src*='bead']는 구현에 강하게 결합됩니다. 대체로 alt 기반 Role 선택자 또는 TestID가 낫습니다. 또한 동일 검증을 TestHelpers로 위임해 중복을 줄이세요.- const physicsImages = page.locator("img[src*='bead']"); + const physicsImages = page.getByRole("img", { name: /bead/i });Alt 텍스트가 없다면
data-testid="physics-image"로 노출 후getByTestId사용을 권장합니다. 더 나아가await TestHelpers.checkPhysicsElements(page)로 캡슐화하면 재사용성이 올라갑니다.
81-84: 애니메이션 컨테이너도 TestID로 고정클래스명 변경에 취약합니다. TestID로 교체 권장.
- await expect(page.locator("[class*='floating-stars']")).toBeVisible(); + await expect(page.getByTestId("floating-stars")).toBeVisible();UI에는
data-testid="floating-stars"속성 추가를 요청드립니다.
86-98: 반응형 검증을 헬퍼로 추출해 중복 제거뷰포트 변경 + 동일 단언이 반복됩니다. 헬퍼로 위임하면 가독성과 유지보수성이 향상됩니다.
- test("should be responsive on different screen sizes", async ({ page }) => { - // Test mobile viewport - await page.setViewportSize({ width: 375, height: 667 }); - await expect(page.locator("main")).toBeVisible(); - - // Test tablet viewport - await page.setViewportSize({ width: 768, height: 1024 }); - await expect(page.locator("main")).toBeVisible(); - - // Test desktop viewport - await page.setViewportSize({ width: 1920, height: 1080 }); - await expect(page.locator("main")).toBeVisible(); - }); + test("should be responsive on different screen sizes", async ({ page }) => { + await TestHelpers.testResponsiveDesign(page, [ + { width: 375, height: 667 }, // mobile + { width: 768, height: 1024 }, // tablet + { width: 1920, height: 1080 }, // desktop + ]); + });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
tests/README.md(1 hunks)tests/home.spec.ts(1 hunks)tests/navigation.spec.ts(1 hunks)tests/utils/test-helpers.ts(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- tests/utils/test-helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/navigation.spec.ts
🧰 Additional context used
🧬 Code graph analysis (1)
tests/home.spec.ts (1)
app/(main)/_components/home-button-section/home-button-section.css.ts (1)
exploreButton(43-52)
🪛 markdownlint-cli2 (0.17.2)
tests/README.md
7-7: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: deploy
- GitHub Check: test
| test("should display user count in caption section", async ({ page }) => { | ||
| // Wait for the user count to be loaded | ||
| await page.waitForTimeout(2000); | ||
|
|
||
| // Check if user count is displayed (it should be a number) | ||
| const userCountElement = page.locator("text=/\\d+/").first(); | ||
| await expect(userCountElement).toBeVisible(); | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
플레이크 유발 waitForTimeout 제거 — 조건 기반 대기로 전환
고정 지연은 CI 환경에서 불안정합니다. TestID를 노출하고 값 패턴을 직접 검증해 주세요.
- // Wait for the user count to be loaded
- await page.waitForTimeout(2000);
-
- // Check if user count is displayed (it should be a number)
- const userCountElement = page.locator("text=/\\d+/").first();
- await expect(userCountElement).toBeVisible();
+ // 사용자 수 로드 대기 (조건 기반)
+ const userCountElement = page.getByTestId("user-count");
+ await expect(userCountElement).toHaveText(/^\d{1,3}(,\d{3})*$/);UI 측에 data-testid="user-count" 추가가 필요합니다. 가능하면 로딩 완료 신호(예: data-loaded="true" 또는 네트워크 idle)로 대기 조건을 보강하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test("should display user count in caption section", async ({ page }) => { | |
| // Wait for the user count to be loaded | |
| await page.waitForTimeout(2000); | |
| // Check if user count is displayed (it should be a number) | |
| const userCountElement = page.locator("text=/\\d+/").first(); | |
| await expect(userCountElement).toBeVisible(); | |
| }); | |
| test("should display user count in caption section", async ({ page }) => { | |
| // 사용자 수 로드 대기 (조건 기반) | |
| const userCountElement = page.getByTestId("user-count"); | |
| await expect(userCountElement).toHaveText(/^\d{1,3}(,\d{3})*$/); | |
| }); |
🤖 Prompt for AI Agents
In tests/home.spec.ts around lines 62 to 69, remove the hardcoded await
page.waitForTimeout(2000) and instead target a stable test hook: add
data-testid="user-count" to the UI element that displays the user count (and if
possible a loading flag like data-loaded="true" or use network idle) and in the
test use a test-id selector (e.g. getByTestId or
locator('[data-testid="user-count"]')) with a conditional wait/assert such as
waiting for it to be visible and toHaveText matching a number regex (/\d+/) so
the test waits for the real readiness signal rather than a fixed timeout.
| ├── auth.spec.ts # 인증 기능 테스트 | ||
| ├── navigation.spec.ts # 네비게이션 테스트 |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
README의 파일 목록과 실제 파일 존재 여부 확인 필요
auth.spec.ts가 목록에 있지만 실제 PR에 포함되지 않았습니다. 문서와 코드의 싱크를 맞춰 주세요. 존재하지 않으면 목록에서 제외하거나 “추가 예정”으로 표기하는 것이 좋습니다.
다음 스크립트로 파일 존재 여부를 확인할 수 있습니다:
🏁 Script executed:
#!/bin/bash
fd -t f -g "tests/auth.spec.ts" -H || echo "tests/auth.spec.ts: not found"Length of output: 34
파일 존재 여부를 확실히 확인하기 위해 아래 스크립트를 실행해 주세요:
🏁 Script executed:
#!/bin/bash
# tests 디렉토리 파일 목록 확인
echo "🔍 tests 디렉토리 파일 목록:"
ls -1 tests
# fd로 auth.spec.ts 검색
echo "🔍 fd로 auth.spec.ts 검색:"
fd -t f auth.spec.ts -H || echo "auth.spec.ts: not found"
# find로 auth.spec.ts 검색
echo "🔍 find로 auth.spec.ts 검색:"
find tests -type f -name auth.spec.ts || echo "auth.spec.ts: not found"Length of output: 351
테스트 파일 목록 동기화 필요
현재 tests/README.md (13–14라인)에 auth.spec.ts가 나열되어 있지만, 실제로는 tests 디렉토리에 해당 파일이 없습니다. 문서와 코드가 어긋나면 유지보수가 어려워지므로, 아래처럼 수정해주세요.
- 파일: tests/README.md
- 라인: 13–14
- ├── auth.spec.ts # 인증 기능 테스트
+ # ├── auth.spec.ts # 인증 기능 테스트 (추가 예정)또는
- ├── auth.spec.ts # 인증 기능 테스트— 목록에서 완전히 제거
위 두 가지 중 하나를 선택해 반영해 주세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ├── auth.spec.ts # 인증 기능 테스트 | |
| ├── navigation.spec.ts # 네비게이션 테스트 | |
| ├── navigation.spec.ts # 네비게이션 테스트 |
🤖 Prompt for AI Agents
In tests/README.md around lines 13–14, the README lists auth.spec.ts which does
not exist in the tests directory; either add the missing tests/auth.spec.ts file
to the repo (with appropriate content and link) or remove the auth.spec.ts line
from the README so the document matches the actual test files—pick one approach
and update the README accordingly to keep the list in sync with the filesystem.
📌 Summary
📚 Tasks
👀 To Reviewer
일단 진행중..
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Style
Refactor
Tests
Documentation