import { expect, test, type Page, type Response } from "@playwright/test"; import { FILLED_REVIEW_SESSION_ID, routeFilledSessionReview } from "./session-review-fixture"; import { expectNoHorizontalOverflow, fetchAvailablePersona, signInAsLearner } from "./support"; interface SessionStartResponse { session_id: string; } async function expectResponseOk(response: { ok: () => boolean; text: () => Promise }) { if (!response.ok()) { expect(response.ok(), await response.text()).toBeTruthy(); } } function isReviewResponse(sessionId: string) { return (response: Response) => { const url = new URL(response.url()); return ( response.request().method() === "GET" && url.pathname.endsWith(`/sessions/${sessionId}/review`) ); }; } async function createEndedSession(page: Page) { const persona = await fetchAvailablePersona(page, 1); const start = await page.request.post("/api/sessions", { data: { persona_code: persona.code, theory_mode: "humanistic", }, }); await expectResponseOk(start); const session = (await start.json()) as SessionStartResponse; const ended = await page.request.post(`/api/sessions/${session.session_id}/end`); await expectResponseOk(ended); return session.session_id; } async function expectReviewRegionsReachable(page: Page) { await expect(page.locator(".sr-overview")).toBeVisible(); await expect(page.locator(".sr-feedback")).toBeVisible(); await expect(page.locator(".sr-card--chart")).toBeVisible(); await expect(page.locator(".sr-card--flow")).toBeVisible(); await expect(page.locator(".sr-card--rubric")).toBeVisible(); await expect(page.locator(".sr-card--transcript")).toBeVisible(); const grid = await page.locator(".sr-cols").evaluate((el) => { const style = window.getComputedStyle(el); return { display: style.display, areas: style.gridTemplateAreas, }; }); expect(grid.display).toBe("grid"); expect(grid.areas).toContain("overview"); expect(grid.areas).toContain("feedback"); expect(grid.areas).toContain("transcript"); expect(grid.areas).not.toContain("teacher"); await expect(page.locator(".sr-overview")).toBeInViewport(); await page.locator(".sr-card--transcript").scrollIntoViewIfNeeded(); await expect(page.locator(".sr-card--transcript")).toBeInViewport(); const railGeometry = await page.locator(".sr-cols").evaluate((gridEl) => { const overview = gridEl.querySelector(".sr-overview")?.getBoundingClientRect(); const transcript = gridEl.querySelector(".sr-card--transcript")?.getBoundingClientRect(); const columns = window .getComputedStyle(gridEl) .gridTemplateColumns.split(" ") .filter(Boolean).length; return { columns, overviewRight: overview ? Math.ceil(overview.right) : 0, transcriptLeft: transcript ? Math.floor(transcript.left) : 0, }; }); if (railGeometry.columns >= 3) { expect(railGeometry.overviewRight).toBeLessThanOrEqual(railGeometry.transcriptLeft); } } test.describe("session review", () => { test("renders server review data without legacy transcript fixtures", async ({ page }) => { await signInAsLearner(page); const sessionId = await createEndedSession(page); const reviewResponsePromise = page.waitForResponse(isReviewResponse(sessionId)); await page.goto(`/learn/session/${sessionId}/review`); const reviewResponse = await reviewResponsePromise; await expectResponseOk(reviewResponse); const review = await reviewResponse.json(); expect(review.session_id).toBe(sessionId); expect(review.reviewReady).toBe(false); expect(review.turns).toHaveLength(0); await expect(page).toHaveURL(new RegExp(`/learn/session/${sessionId}/review$`)); await expect(page.getByText("축어록 없음")).toBeVisible(); await expect(page.getByText("감정 타임라인 대기")).toBeVisible(); await expect(page.getByText("개선점 대기")).toBeVisible(); await expect(page.getByRole("button", { name: "오디오 다시 듣기" })).toBeDisabled(); await expect(page.getByRole("button", { name: "PDF 내보내기" })).toBeDisabled(); await expect(page.getByText("32분 14초")).toHaveCount(0); await expect(page.getByText("시연")).toHaveCount(0); await expectReviewRegionsReachable(page); const filterMetrics = await page.locator(".sr-chip-toggle").evaluateAll((buttons) => buttons.map((button) => { const rect = button.getBoundingClientRect(); const style = window.getComputedStyle(button); return { height: Math.ceil(rect.height), whiteSpace: style.whiteSpace, }; }), ); expect(filterMetrics.every((item) => item.height <= 34 && item.whiteSpace === "nowrap")).toBe( true, ); await expectNoHorizontalOverflow(page); }); test("renders filled review evidence state without clipping", async ({ page }) => { await signInAsLearner(page); await routeFilledSessionReview(page); const reviewResponsePromise = page.waitForResponse(isReviewResponse(FILLED_REVIEW_SESSION_ID)); await page.goto(`/learn/session/${FILLED_REVIEW_SESSION_ID}/review`); const reviewResponse = await reviewResponsePromise; await expectResponseOk(reviewResponse); const review = await reviewResponse.json(); expect(review.session_id).toBe(FILLED_REVIEW_SESSION_ID); expect(review.reviewReady).toBe(true); expect(review.turns).toHaveLength(6); await expect(page).toHaveURL(new RegExp(`/learn/session/${FILLED_REVIEW_SESSION_ID}/review$`)); await expect(page.getByText("관계 신호가 안정적으로 회복된 회기")).toBeVisible(); await expect(page.getByText("감정 밸런스 타임라인")).toBeVisible(); await expect(page.getByText("회기 흐름")).toBeVisible(); await expect(page.getByText("세션 트랜스크립트")).toBeVisible(); await expect(page.getByText("기법 사용 분포")).toBeVisible(); await expect(page.getByText("사례개념화 워크시트")).toBeVisible(); await expect(page.getByText("감정 반영").first()).toBeVisible(); await expect(page.getByText("AI 슈퍼바이저").first()).toBeVisible(); await expect(page.getByText("다음에 시도할 문장")).toBeVisible(); await expect(page.getByText("방어를 낮춘 감정 반영")).toBeVisible(); await expect(page.getByText("개입 전 준비도 확인")).toBeVisible(); await expect(page.locator(".sr-chart__svg")).toBeVisible(); await expect(page.locator(".sr-ws-input").first()).toBeVisible(); await expect(page.getByRole("button", { name: "오디오 다시 듣기" })).toBeEnabled(); await expect(page.getByRole("button", { name: "PDF 내보내기" })).toBeEnabled(); await expectReviewRegionsReachable(page); await expectNoHorizontalOverflow(page); }); });