DevTools 브라우저 검증 구현 계획
Claude용: 필수 하위 스킬:
superpowers:executing-plans,superpowers:test-driven-development,superpowers:verification-before-completion를 사용해 이 계획을 작업 단위로 실행한다.
목표: DevTools 패널이 실제 Vite 개발 서버와 브라우저에서 mount, 표시, transport 수집, export event까지 동작하는지 Playwright로 검증한다.
아키텍처: root에는 Playwright 설정과 e2e/ 테스트를 둔다. examples/devtools-browser-fixture는 실제 Vite 앱으로 만들고 @browse-sent-event/plugin-vite를 사용해 runtime bootstrap을 검증한다. closed Shadow DOM 내부는 직접 DOM query하지 않고, host attribute, screenshot, bounding box, accessibility/snapshot 가능한 표면, fixture probe API를 조합해 검증한다.
기술 스택: Playwright 1.60, Vite 8, TypeScript 6, pnpm workspace, Turborepo, Node.js built-in HTTP/WebSocket fixture.
진행 기록
- 2026-05-25: Playwright 설정, DevTools browser fixture, seeded panel 시각 회귀 테스트를 구현했다.
- 2026-05-25: 실제 브라우저에서 fetch ReadableStream, EventSource, WebSocket 수집 경로를 검증하도록 확장했다.
- 2026-05-25: PR/push에서 단위 테스트, 타입체크, 빌드, 브라우저 E2E, lint, format을 실행하는 CI workflow를 연결했다.
- 2026-05-25: Playwright screenshot baseline이 OS별 파일명으로 분리되므로, Linux CI에서는 기능 E2E만 실행하고 시각 snapshot 비교는 로컬 baseline 검증으로 제한했다.
의식적 부채: CI 시각 snapshot
- 포기하는 것: Linux CI에서 seeded DevTools panel screenshot을 직접 비교하는 검증.
- 왜 지금은 감당 가능한가: CI는 panel mount, seeded data count, 실제 fetch stream/EventSource/WebSocket 수집 경로를 계속 검증한다. 시각 회귀는 macOS 기준 snapshot으로 로컬에서 유지된다.
- 회수 시점: Linux snapshot baseline을 생성해 커밋하거나, Playwright 실행 환경을 컨테이너/폰트까지 고정해 OS 차이를 제거할 때 회수한다.
릴리즈 기준
- 첫 npm 배포 전에는 기능 E2E 검증을 릴리즈 필수 조건으로 유지한다.
- Linux CI 시각 snapshot 비교는 후속 회수 부채로 유지하되, 패널 UI가 공개 품질 기준의 핵심 표면이 되는 시점에는 릴리즈 차단 조건으로 격상한다.
- export 검색어 필터 반영 여부는 별도 UX polish 작업에서 결정한다. 검색 결과와 export 결과의 불일치가 사용자 혼란을 만들 수 있으므로, 첫 공개 배포 전에는 해결하거나 README의 제한사항에 명시한다.
현재 코드 기준
packages/plugin-vite/src/injection.ts는 dev entry 앞에virtual:browse-sent-event/bootstrap을 주입한다.- bootstrap은
installBrowseSentEvent()를 옵션 없이 호출하므로 패널 기본 상태는autoOpen: false다. installBrowseSentEvent()는window.__browseSentEventRuntime__에 runtime을 저장하고bse-devtools-panel을 body에 mount한다.bse-devtools-panel은 closed Shadow DOM을 사용한다.- panel host는
setOpen(open: boolean)public method와openreflected attribute를 가진다. - 현재 테스트는 happy-dom 단위 테스트와 Vite middleware fixture 중심이며, 실제 브라우저 screenshot/e2e 검증은 없다.
구현 계획
작업 1: Playwright root 설정 추가
파일:
- 생성:
playwright.config.ts - 수정:
package.json - 수정:
turbo.json
단계 1: root script 추가
package.json에 e2e script를 추가한다.
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:update": "playwright test --update-snapshots"
}
}단계 2: Playwright config 작성
playwright.config.ts를 생성한다.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "e2e",
fullyParallel: false,
reporter: [["list"]],
use: {
baseURL: "http://127.0.0.1:4174",
trace: "retain-on-failure",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium-desktop",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 720 },
},
},
{
name: "chromium-mobile",
use: {
...devices["Pixel 7"],
},
},
],
webServer: {
command: "pnpm --filter @browse-sent-event/devtools-browser-fixture dev --host 127.0.0.1 --port 4174",
url: "http://127.0.0.1:4174",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});단계 3: turbo task 추가
turbo.json에 test:e2e task를 추가한다.
{
"tasks": {
"test:e2e": {
"dependsOn": ["build"],
"outputs": ["playwright-report/**", "test-results/**"]
}
}
}단계 4: 검증
실행:
pnpm test:e2e기대 결과:
- 아직
e2e테스트와 fixture가 없어 실패한다.
커밋:
git add package.json turbo.json playwright.config.ts
git commit -m "test(e2e): Playwright 브라우저 검증 설정 추가"작업 2: DevTools browser fixture 앱 추가
파일:
- 생성:
examples/devtools-browser-fixture/package.json - 생성:
examples/devtools-browser-fixture/index.html - 생성:
examples/devtools-browser-fixture/src/main.ts - 생성:
examples/devtools-browser-fixture/src/fixture-probe.ts - 생성:
examples/devtools-browser-fixture/tsconfig.json - 생성:
examples/devtools-browser-fixture/vite.config.ts - 수정:
examples/README.md
단계 1: fixture package 생성
examples/devtools-browser-fixture/package.json:
{
"name": "@browse-sent-event/devtools-browser-fixture",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@browse-sent-event/core": "workspace:*",
"@browse-sent-event/plugin-vite": "workspace:*"
},
"devDependencies": {
"vite": "^8.0.14"
}
}단계 2: Vite config 생성
examples/devtools-browser-fixture/vite.config.ts:
import { defineConfig } from "vite";
import browseSentEvent from "@browse-sent-event/plugin-vite";
export default defineConfig({
plugins: [browseSentEvent()],
});단계 3: fixture probe 작성
examples/devtools-browser-fixture/src/fixture-probe.ts는 runtime과 panel host를 테스트가 안전하게 조작할 수 있게 한다.
import type { BrowseSentEventRuntime } from "@browse-sent-event/core";
interface BrowseSentEventPanelHost extends HTMLElement {
setOpen(open: boolean): void;
}
function getRuntime(): BrowseSentEventRuntime {
const runtime = Reflect.get(globalThis.window, "__browseSentEventRuntime__");
if (!runtime) {
throw new Error("browse-sent-event runtime is not installed");
}
return runtime as BrowseSentEventRuntime;
}
function getPanel(): BrowseSentEventPanelHost {
const panel = document.querySelector("bse-devtools-panel");
if (!panel || !("setOpen" in panel)) {
throw new Error("browse-sent-event panel is not mounted");
}
return panel as BrowseSentEventPanelHost;
}
export function seedPanel(): void {
const runtime = getRuntime();
const connection = runtime.engine.recordConnection({
openedAt: 1_000,
protocol: "websocket",
state: "open",
url: "wss://fixture.test/socket",
});
runtime.engine.recordMessage({
connectionId: connection.id,
direction: "out",
payload: "client hello",
protocol: "websocket",
timestamp: 1_100,
type: "message",
});
runtime.engine.recordMessage({
connectionId: connection.id,
direction: "in",
payload: "server hello",
protocol: "websocket",
timestamp: 1_200,
type: "message",
});
getPanel().setOpen(true);
}
export function closePanel(): void {
getPanel().setOpen(false);
}
export function getSnapshotCounts(): { connections: number; messages: number } {
const snapshot = getRuntime().engine.getSnapshot();
return {
connections: snapshot.connections.length,
messages: snapshot.messages.length,
};
}단계 4: main entry 작성
examples/devtools-browser-fixture/src/main.ts:
import { closePanel, getSnapshotCounts, seedPanel } from "./fixture-probe.js";
declare global {
interface Window {
__bseFixture: {
closePanel(): void;
getSnapshotCounts(): { connections: number; messages: number };
seedPanel(): void;
};
}
}
window.__bseFixture = {
closePanel,
getSnapshotCounts,
seedPanel,
};
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<main>
<h1>browse-sent-event browser fixture</h1>
<button id="seed" type="button">Seed panel</button>
</main>
`;
document.querySelector<HTMLButtonElement>("#seed")!.addEventListener("click", () => {
window.__bseFixture.seedPanel();
});단계 5: typecheck 검증
실행:
pnpm --filter @browse-sent-event/devtools-browser-fixture typecheck기대 결과:
- TypeScript typecheck가 통과한다.
커밋:
git add examples/README.md examples/devtools-browser-fixture
git commit -m "test(e2e): DevTools 브라우저 fixture 추가"작업 3: 패널 mount와 시각 회귀 테스트 추가
파일:
- 생성:
e2e/devtools-panel.spec.ts
단계 1: 실패하는 테스트 작성
e2e/devtools-panel.spec.ts:
import { expect, test } from "@playwright/test";
test("mounts the closed-shadow DevTools panel host", async ({ page }) => {
await page.goto("/");
const panel = page.locator("bse-devtools-panel");
await expect(panel).toHaveCount(1);
await expect(panel).not.toHaveAttribute("open", "");
});
test("renders seeded transport data in the panel", async ({ page }) => {
await page.goto("/");
await page.evaluate(() => {
window.__bseFixture.seedPanel();
});
const panel = page.locator("bse-devtools-panel");
const counts = await page.evaluate(() => window.__bseFixture.getSnapshotCounts());
const box = await panel.boundingBox();
expect(counts).toEqual({ connections: 1, messages: 2 });
await expect(panel).toHaveAttribute("open", "");
expect(box?.width).toBeGreaterThan(300);
expect(box?.height).toBeGreaterThan(300);
await expect(panel).toHaveScreenshot("devtools-panel-seeded.png", {
animations: "disabled",
});
});단계 2: RED 확인
실행:
pnpm test:e2e기대 결과:
- screenshot snapshot이 없어서 실패한다.
단계 3: snapshot 생성
실행:
pnpm test:e2e:update기대 결과:
e2e/devtools-panel.spec.ts-snapshots/아래에 desktop/mobile snapshot이 생성된다.
단계 4: GREEN 확인
실행:
pnpm test:e2e기대 결과:
- Chromium desktop/mobile에서 테스트가 통과한다.
커밋:
git add e2e/devtools-panel.spec.ts e2e/devtools-panel.spec.ts-snapshots
git commit -m "test(e2e): DevTools 패널 시각 검증 추가"작업 4: 실제 fetch stream과 EventSource 브라우저 경로 검증
파일:
- 수정:
examples/devtools-browser-fixture/vite.config.ts - 수정:
examples/devtools-browser-fixture/src/fixture-probe.ts - 수정:
e2e/devtools-panel.spec.ts
단계 1: Vite middleware fixture 추가
vite.config.ts에 fixture endpoint를 추가한다.
import type { Connect } from "vite";
function writeStream(res: Connect.ServerResponse, chunks: readonly string[]): void {
res.statusCode = 200;
res.setHeader("content-type", "text/plain; charset=utf-8");
for (const chunk of chunks) {
res.write(chunk);
}
res.end();
}
function writeSse(res: Connect.ServerResponse, chunks: readonly string[]): void {
res.statusCode = 200;
res.setHeader("content-type", "text/event-stream; charset=utf-8");
for (const chunk of chunks) {
res.write(`data: ${chunk}\n\n`);
}
res.end();
}configureServer에서 /__bse-fixture/stream과 /__bse-fixture/events를 처리한다.
단계 2: browser probe 추가
fixture-probe.ts에 실제 browser API 호출 함수를 추가한다.
export async function runFetchStream(): Promise<void> {
const response = await fetch("/__bse-fixture/stream");
await response.text();
}
export async function runEventSource(): Promise<void> {
await new Promise<void>((resolve, reject) => {
const source = new EventSource("/__bse-fixture/events");
let count = 0;
source.onmessage = () => {
count += 1;
if (count >= 2) {
source.close();
resolve();
}
};
source.onerror = () => {
source.close();
reject(new Error("EventSource fixture failed"));
};
});
}단계 3: e2e 테스트 추가
test("records fetch stream and EventSource messages in a real browser", async ({ page }) => {
await page.goto("/");
await page.evaluate(async () => {
await window.__bseFixture.runFetchStream();
await window.__bseFixture.runEventSource();
});
const counts = await page.evaluate(() => window.__bseFixture.getSnapshotCounts());
expect(counts.connections).toBeGreaterThanOrEqual(2);
expect(counts.messages).toBeGreaterThanOrEqual(2);
});단계 4: 검증
실행:
pnpm test:e2e
pnpm test기대 결과:
- e2e와 기존 단위 테스트가 모두 통과한다.
커밋:
git add examples/devtools-browser-fixture e2e/devtools-panel.spec.ts
git commit -m "test(e2e): 브라우저 transport 수집 검증 추가"작업 5: WebSocket 브라우저 경로 검증
파일:
- 생성:
e2e/support/websocket-fixture.ts - 수정:
playwright.config.ts - 수정:
examples/devtools-browser-fixture/src/fixture-probe.ts - 수정:
e2e/devtools-panel.spec.ts
단계 1: 의존성 없는 WebSocket fixture server 작성
Node.js 표준 node:http와 node:crypto만 사용해 최소 WebSocket handshake와 text frame echo를 구현한다. 새 npm dependency를 추가하지 않는다.
단계 2: Playwright global setup 또는 test fixture 연결
테스트 시작 전에 WebSocket server를 띄우고, 종료 시 닫는다.
단계 3: browser probe 추가
export async function runWebSocket(url: string): Promise<void> {
await new Promise<void>((resolve, reject) => {
const socket = new WebSocket(url);
socket.onopen = () => socket.send("browser hello");
socket.onmessage = () => {
socket.close();
resolve();
};
socket.onerror = () => reject(new Error("WebSocket fixture failed"));
});
}단계 4: e2e 테스트 추가
test("records WebSocket messages in a real browser", async ({ page }) => {
await page.goto("/");
await page.evaluate(async (url) => {
await window.__bseFixture.runWebSocket(url);
}, process.env.BSE_WS_FIXTURE_URL);
const counts = await page.evaluate(() => window.__bseFixture.getSnapshotCounts());
expect(counts.connections).toBeGreaterThanOrEqual(1);
expect(counts.messages).toBeGreaterThanOrEqual(2);
});의식적 부채:
- 포기하는 것: 완전한 WebSocket protocol server 구현.
- 감당 가능한 이유: 검증 목적은 browser
WebSocketAPI와 interceptor 기록 경로이며, echo text frame만 있으면 충분하다. - 회수 시점: binary frame, close code, reconnect UI를 검증하는 Phase 1 polish/e2e 확장 시점.
커밋:
git add playwright.config.ts e2e examples/devtools-browser-fixture
git commit -m "test(e2e): WebSocket 브라우저 수집 검증 추가"작업 6: CI와 문서 연결
파일:
- 수정:
.github/workflows/ci.yml또는 생성:.github/workflows/ci.yml - 수정:
README.md - 수정:
docs/index.md - 수정:
docs/plans/2026-05-25-devtools-browser-verification.md
단계 1: CI workflow 확인 또는 생성
현재 저장소에는 docs workflow만 있으므로, CI workflow가 없다면 생성한다.
name: CI
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: 24.13.0
cache: pnpm
- run: corepack enable
- run: corepack prepare pnpm@11.2.2 --activate
- run: pnpm install --frozen-lockfile
- run: pnpm audit --audit-level moderate
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm test
- run: pnpm exec turbo run typecheck --force
- run: pnpm exec turbo run build --force
- run: pnpm test:e2e
- run: pnpm lint
- run: pnpm format:check단계 2: README 업데이트
개발 명령에 pnpm test:e2e를 추가한다.
단계 3: 문서 사이트 index 업데이트
계획 문서 링크에 이 계획을 추가한다.
커밋:
git add .github/workflows/ci.yml README.md docs/index.md docs/plans/2026-05-25-devtools-browser-verification.md
git commit -m "ci(test): 브라우저 검증 workflow 추가"검증 계획
로컬 검증
pnpm install --frozen-lockfile
pnpm audit --json
pnpm peers check
pnpm --filter @browse-sent-event/core build
pnpm --filter @browse-sent-event/plugin-vite build
pnpm --filter @browse-sent-event/devtools-browser-fixture typecheck
pnpm test:e2e
pnpm test
pnpm exec turbo run typecheck --force
pnpm exec turbo run build --force
pnpm lint
pnpm format:check
git diff --check브라우저 화면 검증
- Desktop viewport
1280x720에서 panel이 viewport 안에 들어온다. - Mobile viewport에서 panel
max-width: calc(100vw - 32px)가 지켜진다. - seeded 상태의 panel screenshot이 비어 있지 않고 snapshot과 일치한다.
- panel host의
openattribute가 public method와 함께 토글된다. - closed Shadow DOM 내부를 테스트 편의로 open mode로 바꾸지 않는다.
공급망 검증
- 새 third-party runtime dependency를 추가하지 않는다.
- WebSocket fixture는 Node 표준 모듈만 사용한다.
pnpm audit --jsonvulnerability count가 모두 0이어야 한다.pnpm install --frozen-lockfile이 pnpm 11 supply-chain policy 검사를 통과해야 한다.
비범위
- 디자인 polish, resize, 위치 기억 구현.
- export 검색어 필터 부채 회수.
- binary WebSocket frame 검증.
- cross-browser matrix 전체 확장. 첫 배치는 Chromium desktop/mobile로 제한한다.