Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
124 changes: 122 additions & 2 deletions e2e/mobile-responsive.web.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ test.describe('#221 모바일 반응형(PR1 셸) — 390×844', () => {
['채팅', '.chat', '.chat-main'],
['프로젝트', '.project-layout', '.project-main'],
] as const) {
test(`G1/G9 ${tab} 탭 폰 1열 스택 + .main 가로 오버플로 0 — PR2 활성화`, async ({ page }) => {
test.fail(true, `PR2(#221): ${layoutSel} 232px 그리드 미수정 — 1열 전환 후 활성화`)
test(`G1/G9 ${tab} 탭 폰 1열 스택 + .main 가로 오버플로 0`, async ({ page }) => {
// PR2(#221): test.fail 해제 — 1열 전환 후 강제 활성화(계획 §3 T1).
await openApp(page, server.url, 390, 844)
await switchTab(page, tab)
const o = await overflowOf(page)
Expand Down Expand Up @@ -136,6 +136,51 @@ test.describe('#221 모바일 반응형(PR1 셸) — 390×844', () => {
await expect(page.locator('.project-main')).toHaveCount(1)
})

// G10(Codex 스펙 1R 하드 계약): 시드 방 1개는 스트립이 오버플로하지 않아 vacuous — UI 경로로
// 방을 늘려 오버플로 실존을 선단언한 뒤, 실 스크롤로 마지막 칩의 도달·클릭(조작성)을 증명한다.
test('G10 채팅 .rooms 칩 스트립 — 방 3개+ 생성→오버플로 실존→마지막 칩 도달·클릭·활성', async ({
page,
}) => {
await openApp(page, server.url, 390, 844)
await switchTab(page, '채팅')
for (let i = 1; i <= 3; i++) {
await page.getByPlaceholder('새 방 이름').fill(`모바일-긴-작업방-이름-${i}`)
await page.getByRole('button', { name: '+', exact: true }).click()
await expect(page.locator('.room-btn')).toHaveCount(1 + i) // 시드 1 + 생성분
}
const rooms = page.locator('.chat .rooms')
const overflow = await rooms.evaluate((el) => el.scrollWidth - el.clientWidth)
expect(overflow, '.rooms 가로 오버플로 실존(스트립 스크롤 표면)').toBeGreaterThan(0)
// 프로그램적 scrollLeft 는 overflow:hidden 에서도 동작 — 사용자 스크롤 가능성은 computed 로
// 이중화(자체 적대 리뷰 P3-2: auto→hidden 회귀는 행동 단언만으론 GREEN).
const overflowX = await rooms.evaluate((el) => getComputedStyle(el).overflowX)
expect(overflowX, '.rooms overflow-x(사용자 스크롤 가능성)').toBe('auto')
// 실 스크롤 → 마지막 칩이 뷰포트/스트립 안에 들어와 클릭 가능해진다.
await rooms.evaluate((el) => {
el.scrollLeft = el.scrollWidth
})
const last = page.locator('.room-btn').last()
const box = await last.boundingBox()
expectInViewport(box, 390, 844, '마지막 룸 칩')
await last.click()
await expect(last).toHaveAttribute('data-active', 'true')
// G12: 방 선택 상태에서 composer 입력창이 뷰포트 안(엄지 도달).
const composer = page.getByPlaceholder('메시지 입력 (개입/지시)')
expectInViewport(await composer.boundingBox(), 390, 844, 'composer 입력창')
})

// G11: 프로젝트 폼 조작 가능(.grid-2 1열 전환 후 goal 입력이 실제로 동작).
test('G11 프로젝트 goal 입력 가능(폰)', async ({ page }) => {
await openApp(page, server.url, 390, 844)
await switchTab(page, '프로젝트')
const goal = page.getByPlaceholder(/사용자 인증/)
await goal.fill('모바일 폼 입력 계약')
await expect(goal).toHaveValue('모바일 폼 입력 계약')
// 폴드 아래는 세로 스크롤로 도달하는 게 정상 계약 — 스크롤 후 뷰포트내(폭 이탈·오프스크린만 적발).
await goal.scrollIntoViewIfNeeded()
expectInViewport(await goal.boundingBox(), 390, 844, 'goal 입력창')
})

test('G3 내비 3탭 뷰포트내 앵커 + G4 터치 타깃 ≥44px(내비·위저드 bare 버튼·.btn 표본)', async ({
page,
}) => {
Expand Down Expand Up @@ -255,6 +300,81 @@ test.describe('#221 데스크톱 무회귀 가드 — 641×900(경계 최소 초
expect(computed.fieldFont, '.field(#mcp-servers) font-size').toBe('13px')
expect(computed.bodyFont, 'body font-size').toBe('13px')
expect(computed.tagVisible, '.brand .tag 가시(데스크톱)').toBe(true)
// R1 확장(PR2): 패널 그리드의 232px 사이드바 칼럼이 >640px 에서 유지 — 1열 규칙 유출 즉시 적발.
await switchTab(page, '채팅')
const chatCols = await page
.locator('.chat')
.evaluate((el) => getComputedStyle(el).gridTemplateColumns)
expect(chatCols, '.chat 데스크톱 232px 칼럼').toContain('232px')
await switchTab(page, '프로젝트')
const projCols = await page
.locator('.project-layout')
.evaluate((el) => getComputedStyle(el).gridTemplateColumns)
expect(projCols, '.project-layout 데스크톱 232px 칼럼').toContain('232px')
})
}
})

// G10b(Codex 계획 1R — 생성 메커니즘 확정분): 프로젝트 목록 항목은 런 시작으로만 생긴다(채팅의
// 무비용 방 생성 경로 부재) — `complete` 러너 서버(별도 수명주기·Codex 계획 2R 노트)에서 UI 경로로
// 런을 순차 완주시켜 3건+ 를 만들고, 프로젝트 탭 스트립에 채팅과 동급 하드 계약을 건다.
// src/main/e2e.ts 시드·엔진 API 무변경(isE2EActive 가드 불변).
test.describe('#221 PR2 — complete 러너(프로젝트 스트립·요약 오버플로)', () => {
let csrv: RunningWebServer

test.beforeAll(async () => {
csrv = await startFleetWebServer({ FLEET_E2E_RUNNER: 'complete' })
})
test.afterAll(async () => {
await csrv?.stop()
})

test('G10b 프로젝트 .rooms 칩 스트립 — 런 3회 완주→오버플로 실존→마지막 칩 클릭 + G8 요약 긴 토큰', async ({
page,
}) => {
await openApp(page, csrv.url, 390, 844)
await switchTab(page, '프로젝트')
// 런 3회 순차 완주 — web-orchestration 패턴(각 완주·버튼 재활성 대기 후 다음 런: Codex 계획 2R).
for (let i = 1; i <= 3; i++) {
await page.getByPlaceholder(/사용자 인증/).fill(`모바일-스트립-프로젝트-${i} 목표`)
await page.getByRole('button', { name: '오케스트레이션 실행' }).click()
await expect(page.getByText('최종 요약 / 누락 점검')).toBeVisible({ timeout: 45_000 })
// stale 요약 조기 매치여도 여기가 완주까지 재시도(런 중 라벨='실행 중…' — 자체 리뷰 P3-3):
// 대기 예산을 명시해 기본 10s 로 전가되지 않게 한다.
await expect(page.getByRole('button', { name: '오케스트레이션 실행' })).toBeEnabled({
timeout: 45_000,
})
}
// G8(e2e 층): 요약 표시 상태에서 긴 무공백 토큰을 주입해도 .main 가로 스크롤 0
// (완주 러너 요약이 짧아 주입으로 대체 — 계획 §1 G8 주석 의무. .summary overflow-wrap 이 방어).
// **칩 클릭보다 먼저** — 다른 칩 선택은 setSummary('') 로 .summary 를 unmount 시켜 주입이
// 무음 skip 되는 결정론적 vacuous 였다(자체 적대 리뷰 P1-1). 부재는 throw = 게이트 파손 신호.
await page.evaluate(() => {
const s = document.querySelector('.summary')
if (!s) throw new Error('.summary 부재 — G8 주입 게이트 파손(요약 미표시 상태)')
s.textContent += ' /very/long/unbroken/path/' + 'x'.repeat(600)
})
const o = await page.evaluate(() => {
const main = document.querySelector('.main')
if (!main) throw new Error('.main 부재')
return main.scrollWidth - main.clientWidth
})
expect(o, '긴 토큰 주입 후 .main 가로 오버플로').toBeLessThanOrEqual(0)
// '새 프로젝트' 선택 버튼도 .room-btn — 항목 3 + 선택 버튼 1 = 4(스트립 오버플로에도 기여).
await expect(page.locator('.project-layout .room-btn')).toHaveCount(4)
const rooms = page.locator('.project-layout .rooms')
const overflow = await rooms.evaluate((el) => el.scrollWidth - el.clientWidth)
expect(overflow, '프로젝트 .rooms 가로 오버플로 실존').toBeGreaterThan(0)
await rooms.evaluate((el) => {
el.scrollLeft = el.scrollWidth
})
const last = page.locator('.project-layout .room-btn').last()
// 런 완주 후 .main 이 요약까지 세로 스크롤된 상태 — 스트립까지 스크롤 복귀가 정상 도달 경로.
// scrollIntoViewIfNeeded 는 양축 스크롤이라 **가로 조작성 증명 권위는 G10(채팅)** — 여기는
// 도달성(overflow 실존 + 클릭 가능)만 계약(자체 적대 리뷰 P3-1).
await last.scrollIntoViewIfNeeded()
expectInViewport(await last.boundingBox(), 390, 844, '마지막 프로젝트 칩')
await last.click()
await expect(last).toHaveAttribute('data-active', 'true')
})
})
47 changes: 47 additions & 0 deletions src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1306,4 +1306,51 @@ code.id {
(Codex PR 1R P2 — .summary 와 동종 결함의 위저드 국소판). */
overflow-wrap: anywhere;
}

/* ── PR2: 패널 1열 스택 + 룸/프로젝트 칩 스트립 ─────────────────────────
232px 사이드바 그리드는 390px 에서 본문을 짜부라뜨린다(오버플로 없는 조작 불능 — RED 정직성
실측). 사이드바를 위 행의 가로 칩 스트립으로 전환(C2 .modal-chips 선례 — iOS 모멘텀 기본). */
.chat,
.project-layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr; /* 스트립(auto) + 본문(잔여) — 암묵 auto 행은 본문 높이를 붕괴시킨다 */
}
.chat {
/* base 상수 160px 은 데스크톱 크롬 기준 — 폰 safe-area additive(topbar top·footer bottom)가
붙으면 크롬이 커져 composer 가 폴드 아래로 밀린다(자체 적대 리뷰 P2-1 — Playwright 는 env()=0
이라 못 잡는 실기기 한정 표면). additive 로 늘어난 만큼만 정확히 차감: env=0 이면 현행과 동일. */
height: calc(
100dvh - 160px - max(0px, env(safe-area-inset-top) - 10px) -
max(0px, env(safe-area-inset-bottom) - 7px)
Comment thread
pdw96 marked this conversation as resolved.
);
}
.rooms {
display: flex;
align-items: center;
gap: 8px;
overflow-x: auto;
}
.rooms > * {
flex: 0 0 auto; /* 비버튼 자식(eyebrow·방 생성 .row)도 스트립 안 공존(R4 — 계약 우선 초안 적발) */
}
.rooms .field {
/* width:auto 는 input 고유폭(size 기본·16px 폰트서 ~190px)이라 스트립을 과점유 — 명시 고정
(자체 적대 리뷰 P3-2). */
width: 140px;
}
.room-btn {
width: auto; /* 기본 width:100%(세로 목록) 해제 — 칩 폭은 내용 기반 */
max-width: 220px; /* 긴 제목은 기존 ellipsis 로 절단 */
}

/* 프로젝트 폼·세션 패널·로그 정돈 */
.grid-2 {
grid-template-columns: 1fr; /* elicitation·manual 역할 배정 2열 → 1열 */
}
.line-item {
/* 칩·배지·삭제 버튼 겹침 방지. 주의: SessionsPanel 세션 목록의 인라인 flexWrap(전 뷰포트)이
존치돼 지배 규칙이 갈라져 있다(자체 적대 리뷰 P3-3) — 폰 wrap 정책을 바꿀 땐 그 인라인을
함께 정리할 것(JSX 무변 계약이라 이번 PR 은 CSS 만). */
flex-wrap: wrap;
}
}
29 changes: 29 additions & 0 deletions src/renderer/styles.responsive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,35 @@ describe('#221 ≤640px 셸 블록 — 폰 분기 규칙 존재 핀(미감 수
})
})

describe('#221 PR2 — 패널 1열 스택·칩 스트립·폼 핀(셀렉터 결합)', () => {
it('G9 .chat/.project-layout 폰 1열(232px 사이드바 해제) + rows auto 1fr(본문 높이 계약)', () => {
expect(shell).toMatch(/\.chat,\s*\.project-layout\s*\{[^}]*grid-template-columns: *1fr/)
// load-bearing 선언(암묵 auto 행 = 본문 높이 붕괴)인데 무핀이던 갭(자체 적대 리뷰 P2-1).
expect(shell).toMatch(/\.chat,\s*\.project-layout\s*\{[^}]*grid-template-rows: *auto 1fr/)
})

it('.rooms 가로 칩 스트립(overflow-x auto — C2 .modal-chips 선례)', () => {
expect(shell).toMatch(/\.rooms\s*\{[^}]*overflow-x: *auto/)
})

it('.chat 높이의 safe-area 차감식 — env()=0 인 Playwright 가 못 보는 실기기 보정 핀(CodeRabbit)', () => {
const chatInShell = ruleBlocks(shell, '.chat')
const heightDecl = chatInShell.find((b) => b.includes('height: calc('))
expect(heightDecl, '.chat 높이 calc 재선언 존재').toBeDefined()
// 상·하단 additive 초과분을 각각 max(0px, env - 기본값) 으로 차감 — env=0 이면 현행 기하 불변.
expect(heightDecl).toMatch(/max\(0px,\s*env\(safe-area-inset-top\) - 10px\)/)
expect(heightDecl).toMatch(/max\(0px,\s*env\(safe-area-inset-bottom\) - 7px\)/)
})

it('G11 .grid-2 폰 1열(elicitation·manual 역할 배정)', () => {
expect(shell).toMatch(/\.grid-2\s*\{[^}]*grid-template-columns: *1fr/)
})

it('.line-item 랩(칩·배지·삭제 버튼 겹침 방지)', () => {
expect(shell).toMatch(/\.line-item\s*\{[^}]*flex-wrap: *wrap/)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

describe('#221 R10 — reduced-motion 규율(§6-3: 640px 블록 밖 신규 추가 금지)', () => {
it('640px 블록 밖 prefers-reduced-motion 출현 = 기존 2곳 고정(블록 안 중첩은 자유)', () => {
// 640px 블록 전부를 제거한 나머지에서 카운트 — 전역 블록(:991)·modal-card 블록(:1149)
Expand Down