// frontend/playwright/inbox.spec.ts // 백엔드를 LLM_PROVIDER=heuristic 로 기동한 상태를 가정(결정성·속도·폴백 배지). // 캡처는 인박스 시드를 변경하므로 단일 워커 직렬. import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; test.describe.configure({ mode: "serial" }); test("캡처 → 분류 → 좋아요 → 작업 페이지에 등장(연합)", async ({ page }) => { await page.goto("/inbox"); const input = page.getByLabel("인박스에 빠르게 캡처"); await input.fill("다음 주에 한국 놀러가는 비행기 티켓 사기"); await input.press("Enter"); const fresh = page.locator(".sb-cap.fresh").first(); await expect(fresh.getByText("작업", { exact: true })).toBeVisible(); await expect(fresh.locator(".r-chip.proj")).toContainText("개인 › 여행 — 한국"); await expect(fresh.locator(".r-chip.auto")).toContainText("가격 추적 알림 켜둠"); await fresh.getByRole("button", { name: /좋아요, 그렇게 해줘/ }).click(); await expect(fresh.locator(".sb-done")).toBeVisible(); // 작업 페이지(개인 스코프)에서 확인 — 연합 await page.goto("/tasks"); await page.locator(".tree-row.folder", { hasText: "개인" }).click(); await expect( page.locator(".kcard-title", { hasText: "다음 주에 한국 놀러가는 비행기 티켓 사기" }), ).toBeVisible(); }); test("다르게 분류 — 타입 순환 task→event", async ({ page }) => { await page.goto("/inbox"); const input = page.getByLabel("인박스에 빠르게 캡처"); await input.fill("리포트 회신 보내기"); await input.press("Enter"); const fresh = page.locator(".sb-cap.fresh").first(); await expect(fresh.getByText("작업", { exact: true })).toBeVisible(); await fresh.getByRole("button", { name: /다르게 분류/ }).click(); await expect(fresh.getByText("일정", { exact: true })).toBeVisible(); }); const GOLDEN = [ { raw: "다음 주에 한국 놀러가는 비행기 티켓 사기", type: "작업", proj: "개인 › 여행 — 한국" }, { raw: "수요일 11시 자전거 수리 맡기기", type: "일정", proj: "개인 캘린더" }, { raw: "엄마 생신 선물 미리 알아보기", type: "작업", proj: "가족" }, { raw: "온보딩 환영 화면에 짧은 애니메이션 넣으면 어떨까", type: "아이디어", proj: "아이디어 보드" }, ]; for (const g of GOLDEN) { test(`골든: "${g.raw}" → ${g.type}`, async ({ page }) => { await page.goto("/inbox"); const input = page.getByLabel("인박스에 빠르게 캡처"); await input.fill(g.raw); await input.press("Enter"); const fresh = page.locator(".sb-cap.fresh").first(); await expect(fresh.getByText(g.type, { exact: true })).toBeVisible(); await expect(fresh.locator(".r-chip.proj")).toContainText(g.proj); }); } test("LLM 폴백 — 규칙 기반(오프라인) 배지", async ({ page }) => { await page.goto("/inbox"); const input = page.getByLabel("인박스에 빠르게 캡처"); await input.fill("엄마 생신 선물 미리 알아보기"); await input.press("Enter"); const fresh = page.locator(".sb-cap.fresh").first(); await expect(fresh.getByText("작업", { exact: true })).toBeVisible(); await expect(fresh.getByText("규칙 기반(오프라인)")).toBeVisible(); }); test("음성 스텁 — voice 아이콘 행", async ({ page }) => { await page.goto("/inbox"); await page.getByLabel(/음성으로 캡처/).click(); await expect(page.locator(".cap-k.voice").first()).toBeVisible(); }); test("a11y — axe 위반 없음(color-contrast 제외)", async ({ page }) => { await page.goto("/inbox"); await expect(page.getByRole("heading", { name: "스마트 인박스", exact: true })).toBeVisible(); const r = await new AxeBuilder({ page }) .withTags(["wcag2a", "wcag2aa"]) .disableRules(["color-contrast"]) .analyze(); expect(r.violations).toEqual([]); });