import { expect, test, type Page, type Response } from "@playwright/test"; import { FILLED_REVIEW_SESSION_ID, filledReviewResponse, routeFilledSessionReview, routePrepostMeasures, } 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(); } } async function routeSupervisorCapableLearner(page: Page) { await page.route("**/api/auth/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ user_id: "00000000-0000-0000-0000-000000000303", email: "yunchan@twentyoz.kr", role: "learner", display_name: "Yun Chan", admin_access: true, super_admin: true, account_status: "approved", approval_required: false, cohort_ids: [], consent_at: 1782820000, onboarding_completed_at: 1782820001, nickname: "Yun Chan", self_introduction: "", avatar_url: "", }), }); }); } async function routeLearnerReviewUser(page: Page) { await page.route("**/api/auth/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ user_id: "00000000-0000-0000-0000-000000000101", email: "learner@hs.ac.kr", role: "learner", display_name: "E2E Learner", admin_access: false, super_admin: false, account_status: "approved", approval_required: false, cohort_ids: [], consent_at: 1782820000, onboarding_completed_at: 1782820001, nickname: "E2E Learner", self_introduction: "", avatar_url: "", }), }); }); } 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 openReviewTab(page: Page, label: "축어록" | "피드백" | "워크시트") { // 탭 바는 리뷰 레이아웃이 로드된 뒤에만 DOM에 존재한다 — attach를 먼저 기다린다. const tabs = page.locator(".sr-tabs"); await tabs.waitFor({ state: "attached", timeout: 10_000 }).catch(() => {}); const tab = tabs.locator("button", { hasText: label }); if (await tab.isVisible().catch(() => false)) { await tab.click(); } } async function expectReviewRegionsReachable(page: Page) { await expect(page.locator(".sr-overview")).toBeVisible(); await expect(page.locator(".sr-tabs")).toBeVisible(); // 피드백 탭: 인사이트 흐름 + 평가 레일이 함께 도달 가능해야 한다. await openReviewTab(page, "피드백"); 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--prepost")).toBeVisible(); const insightsGrid = await page.locator(".sr-cols").evaluate((el) => { const style = window.getComputedStyle(el); return { display: style.display, areas: style.gridTemplateAreas, columns: style.gridTemplateColumns.split(" ").filter(Boolean).length, }; }); expect(insightsGrid.display).toBe("grid"); expect(insightsGrid.areas).toContain("insight"); expect(insightsGrid.areas).toContain("feedback"); expect(insightsGrid.areas).not.toContain("teacher"); // 전 폭 탭 계약: 활성 탭 하나가 전폭 단일 컬럼을 쓴다(인사이트 위 / 평가 아래). expect(insightsGrid.columns).toBe(1); const stackGeometry = await page.locator(".sr-cols").evaluate((el) => { const insight = el.querySelector(".sr-left")?.getBoundingClientRect(); const feedback = el.querySelector(".sr-right")?.getBoundingClientRect(); return { insightBottom: insight ? Math.ceil(insight.bottom) : 0, feedbackTop: feedback ? Math.floor(feedback.top) : 0, }; }); expect(stackGeometry.insightBottom).toBeLessThanOrEqual(stackGeometry.feedbackTop + 1); // 워크시트 탭. await openReviewTab(page, "워크시트"); await expect(page.locator(".sr-card--worksheet")).toBeVisible(); // 축어록 탭(기본)으로 복귀 — 축어록은 전폭 단일 컬럼. await openReviewTab(page, "축어록"); await expect(page.locator(".sr-card--transcript")).toBeVisible(); const transcriptAreas = await page .locator(".sr-cols") .evaluate((el) => window.getComputedStyle(el).gridTemplateAreas); expect(transcriptAreas).toContain("transcript"); await page.locator(".sr-overview").scrollIntoViewIfNeeded(); await expect(page.locator(".sr-overview")).toBeInViewport(); await page.locator(".sr-card--transcript").scrollIntoViewIfNeeded(); await expect(page.locator(".sr-card--transcript")).toBeInViewport(); // 리뷰 요약은 본문 그리드 위의 전폭 밴드다(도킹/스크롤 부유 금지). const band = await page.locator(".sr-root").evaluate((rootEl) => { const overview = rootEl.querySelector(".sr-overview")?.getBoundingClientRect(); const gridRect = rootEl.querySelector(".sr-cols")?.getBoundingClientRect(); return { overviewBottom: overview ? Math.ceil(overview.bottom) : 0, gridTop: gridRect ? Math.floor(gridRect.top) : 0, }; }); expect(band.overviewBottom).toBeLessThanOrEqual(band.gridTop + 1); } test.describe("session review", () => { test("redirects supervisor-capable users from learner 403 into the teacher review route", async ({ page, }) => { await routeSupervisorCapableLearner(page); await routePrepostMeasures(page); const sessionId = "supervisor-redirect-review"; let reviewAttempts = 0; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { reviewAttempts += 1; const referer = route.request().headers().referer ?? page.url(); if (!referer.includes(`/teach/session/${sessionId}/review`)) { await route.fulfill({ status: 403, contentType: "application/json", body: JSON.stringify({ detail: "session does not belong to user" }), }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(filledReviewResponse(sessionId)), }); }); await page.goto(`/learn/session/${sessionId}/review`); await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`)); await openReviewTab(page, "피드백"); await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible(); expect(reviewAttempts).toBeGreaterThanOrEqual(2); }); test("normalizes a successful supervisor review response away from the learner route", async ({ page, }) => { await routeSupervisorCapableLearner(page); const sessionId = "supervisor-success-review"; let reviewAttempts = 0; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { reviewAttempts += 1; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ...filledReviewResponse(sessionId), teacherReview: { status: "pending", note: "", reviewedAt: null, reviewerId: null, updatedAt: null, worksheetStatus: "pending", worksheetNote: "", worksheetReviewedAt: null, }, }), }); }); await page.goto(`/learn/session/${sessionId}/review`); await expect(page).toHaveURL(new RegExp(`/teach/session/${sessionId}/review$`)); await openReviewTab(page, "피드백"); await expect(page.getByText("교수자 검토", { exact: true })).toBeVisible(); await expect(page.locator(".sr-card--prepost")).toHaveCount(0); expect(reviewAttempts).toBeGreaterThanOrEqual(2); }); test("shows supervisor evaluation scope boundaries", async ({ page }) => { await routeSupervisorCapableLearner(page); const sessionId = "supervisor-evaluation-scope"; await routeFilledSessionReview(page, sessionId); await page.goto(`/teach/session/${sessionId}/review`); await openReviewTab(page, "피드백"); await expect(page.getByText("AI 평가 범위", { exact: true })).toBeVisible(); await expect(page.getByText("현재 구현", { exact: true })).toBeVisible(); await expect(page.getByText(/fast-loop.*deep-loop/)).toBeVisible(); await expect(page.getByText("임상팀 확정 필요", { exact: true })).toBeVisible(); }); test("refreshes an ended review from evaluation pending to ready", async ({ page }) => { await routeSupervisorCapableLearner(page); await routePrepostMeasures(page); const sessionId = "review-pending-then-ready"; let reviewAttempts = 0; let firstResponseWasPending = false; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { reviewAttempts += 1; const ready = { ...filledReviewResponse(sessionId), summary: "평가 AI가 저장된 축어록을 분석했습니다.", }; const pending = { ...ready, supervisorState: "평가 대기", summary: "평가 AI가 저장된 축어록을 분석 중입니다.", rubric: [], goodMoments: [], growthPoints: [], degraded: true, reviewReady: false, }; if (reviewAttempts === 1) firstResponseWasPending = true; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(reviewAttempts === 1 ? pending : ready), }); }); await page.goto(`/learn/session/${sessionId}/review`); await expect(page.getByText("평가 AI가 저장된 축어록을 분석했습니다.")).toBeVisible({ timeout: 5000, }); await expect(page.getByText("평가 완료")).toBeVisible(); expect(firstResponseWasPending).toBe(true); expect(reviewAttempts).toBeGreaterThanOrEqual(2); }); test("keeps polling long-running session evaluation until the ready review arrives", async ({ page, }) => { await routeSupervisorCapableLearner(page); await routePrepostMeasures(page); const sessionId = "review-delayed-ready"; let reviewAttempts = 0; const readyAtAttempt = 10; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { reviewAttempts += 1; const ready = { ...filledReviewResponse(sessionId), summary: "평가 AI가 늦게 완료된 deep-loop 결과를 반영했습니다.", }; const pending = { ...ready, supervisorState: "평가 대기", summary: "평가 AI가 긴 회기 축어록을 분석 중입니다.", rubric: [], goodMoments: [], growthPoints: [], degraded: true, reviewReady: false, }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(reviewAttempts >= readyAtAttempt ? ready : pending), }); }); await page.goto(`/learn/session/${sessionId}/review`); await expect(page.getByText("평가 AI가 늦게 완료된 deep-loop 결과를 반영했습니다.")).toBeVisible({ timeout: 20_000, }); await expect(page.getByText("평가 완료")).toBeVisible(); expect(reviewAttempts).toBeGreaterThanOrEqual(readyAtAttempt); }); test("does not offer manual AI retry while session evaluation is still pending", async ({ page, }) => { await routeSupervisorCapableLearner(page); await routePrepostMeasures(page); const sessionId = "review-pending-no-manual-retry"; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { const pending = { ...filledReviewResponse(sessionId), supervisorState: "평가 대기", summary: "평가 AI가 저장된 축어록을 분석 중입니다.", rubric: [], goodMoments: [], growthPoints: [], degraded: true, reviewReady: false, }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(pending), }); }); await page.goto(`/teach/session/${sessionId}/review`); await openReviewTab(page, "피드백"); await expect(page.getByText("평가 대기", { exact: true })).toBeVisible(); await expect(page.getByText("평가 AI가 저장된 축어록을 분석 중입니다.")).toBeVisible(); await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toHaveCount(0); await expect(page.getByRole("button", { name: "검토 완료" })).toBeDisabled(); }); test("lets supervisor retry a failed AI session evaluation", async ({ page }) => { await routeSupervisorCapableLearner(page); await routePrepostMeasures(page); const sessionId = "review-failed-retry"; let reevaluateRequests = 0; let retrySucceeded = false; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { const ready = filledReviewResponse(sessionId); const failed = { ...ready, supervisorState: "평가 실패", summary: "저장된 축어록은 확인했지만 평가 AI 산출물이 아직 준비되지 않았습니다. 사유: engine_error: evaluator timeout", rubric: [], goodMoments: [], growthPoints: [], degraded: true, reviewReady: false, teacherReview: { status: "pending", note: "", reviewedAt: null, reviewerId: null, updatedAt: null, worksheetStatus: "pending", worksheetNote: "", worksheetReviewedAt: null, }, }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(retrySucceeded ? ready : failed), }); }); await page.route(`**/api/eval/sessions/${sessionId}/reevaluate`, async (route) => { reevaluateRequests += 1; retrySucceeded = true; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ loop: "deep", session_id: sessionId, stage: "정리", scope: "session_end", turns_evaluated: 2, distribution: {}, theory_mode: "humanistic", strengths: [], improvements: [], intent_deviations: [], error: null, }), }); }); await page.goto(`/teach/session/${sessionId}/review`); await openReviewTab(page, "피드백"); await expect(page.getByText("평가 실패")).toBeVisible(); await expect( page.getByText("저장된 축어록은 확인했지만 deep-loop 평가 AI 산출물을 표시하지 못했습니다. AI 평가 재시도가 필요합니다."), ).toBeVisible(); await expect(page.getByText("engine_error: evaluator timeout")).toHaveCount(0); await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toBeVisible(); await expect(page.getByRole("button", { name: "검토 완료" })).toBeDisabled(); await page.getByRole("button", { name: "AI 평가 재시도" }).click(); await expect(page.getByText("평가 완료")).toBeVisible(); await expect(page.getByText("평가 실패")).toHaveCount(0); await expect(page.getByRole("button", { name: "검토 완료" })).toBeEnabled(); expect(reevaluateRequests).toBe(1); }); test("keeps the failed review visible when AI evaluation retry fails", async ({ page }) => { await routeSupervisorCapableLearner(page); await routePrepostMeasures(page); const sessionId = "review-failed-retry-error"; let reviewRequests = 0; let reevaluateRequests = 0; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { reviewRequests += 1; const ready = filledReviewResponse(sessionId); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ...ready, supervisorState: "평가 실패", summary: "저장된 축어록은 확인했지만 평가 AI 산출물이 아직 준비되지 않았습니다. 사유: session evaluation timeout after 45s", rubric: [], goodMoments: [], growthPoints: [], degraded: true, reviewReady: false, teacherReview: { status: "pending", note: "", reviewedAt: null, reviewerId: null, updatedAt: null, worksheetStatus: "pending", worksheetNote: "", worksheetReviewedAt: null, }, }), }); }); await page.route(`**/api/eval/sessions/${sessionId}/reevaluate`, async (route) => { reevaluateRequests += 1; await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ detail: "engine_error: retry timeout" }), }); }); await page.goto(`/teach/session/${sessionId}/review`); await expect(page.getByText("평가 실패")).toBeVisible(); const reviewRequestsBeforeRetry = reviewRequests; await page.getByRole("button", { name: "AI 평가 재시도" }).click(); await expect( page.getByText("AI 평가 재시도를 완료하지 못했습니다. 잠시 뒤 다시 실행해 주세요."), ).toBeVisible(); await expect(page.getByText("engine_error: retry timeout")).toHaveCount(0); await expect(page.getByText("평가 실패")).toBeVisible(); await expect(page.getByRole("button", { name: "AI 평가 재시도" })).toBeEnabled(); expect(reevaluateRequests).toBe(1); expect(reviewRequests).toBe(reviewRequestsBeforeRetry); }); test("renders server review data without legacy transcript fixtures", async ({ page }) => { await signInAsLearner(page); await routePrepostMeasures(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 openReviewTab(page, "피드백"); 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.whiteSpace === "nowrap")).toBe(true); if ((page.viewportSize()?.width ?? 0) <= 720) { expect(filterMetrics.every((item) => item.height >= 44)).toBe(true); } else { expect(filterMetrics.every((item) => item.height <= 34)).toBe(true); } await expectNoHorizontalOverflow(page); }); test("renders filled review evidence state without clipping", async ({ page }) => { await signInAsLearner(page); await routeFilledSessionReview(page); await routePrepostMeasures(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.getByRole("button", { name: "다시 불러오기" })).toHaveCount(0); 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("감정 반영").first()).toBeVisible(); await expect(page.getByText("AI 슈퍼바이저").first()).toBeVisible(); await openReviewTab(page, "피드백"); await expect(page.getByText("감정 밸런스 타임라인")).toBeVisible(); await expect(page.getByText("회기 흐름")).toBeVisible(); await expect(page.getByText("기법 사용 분포")).toBeVisible(); await expect(page.getByText("파일럿 증거 원장")).toBeVisible(); await expect(page.getByText("1/3 쌍")).toBeVisible(); const prepostInputs = page.locator(".sr-card--prepost input"); await expect(prepostInputs).toHaveCount(6); await expect(prepostInputs.nth(0)).toHaveValue("3"); await expect(prepostInputs.nth(1)).toHaveValue("4"); const prepostLabels = await prepostInputs.evaluateAll((inputs) => inputs.map((input) => input.getAttribute("aria-label")), ); expect(prepostLabels).toEqual([ "자기효능감 사전 점수", "자기효능감 사후 점수", "기술숙련도 사전 점수", "기술숙련도 사후 점수", "수련만족도 사전 점수", "수련만족도 사후 점수", ]); await expect(page.getByText("효과 판정과 통계 검정은 평가설계 확정 후 별도 산출합니다.")).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 openReviewTab(page, "워크시트"); await expect(page.getByText("사례개념화 워크시트")).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); }); test("keeps masked API transcript while naturalizing transcript placeholders", async ({ page, }) => { await routeLearnerReviewUser(page); await routePrepostMeasures(page); const sessionId = "review-placeholder-display"; const response = filledReviewResponse(sessionId); response.turns[0] = { ...response.turns[0], text: "저는 [NAME]이고 [ORG]에서 상담받고 있어요.", }; response.summary = "평가 AI는 [NAME]가 [ORG]에서 보인 조심스러운 반응을 분석했습니다."; if (!response.caseWorksheet) { throw new Error("filled review fixture must include a worksheet"); } response.caseWorksheet.sections[0].items[0].evidence = [ { turnId: "t1", speaker: "client", quote: "저는 [NAME]이고 [ORG]에서 상담받고 있어요.", }, ]; await page.route(`**/api/sessions/${sessionId}/review`, async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(response), }); }); const reviewResponsePromise = page.waitForResponse(isReviewResponse(sessionId)); await page.goto(`/learn/session/${sessionId}/review`); const reviewResponse = await reviewResponsePromise; await expectResponseOk(reviewResponse); const apiReview = await reviewResponse.json(); expect(apiReview.turns[0].text).toContain("[NAME]"); const transcript = page.locator(".sr-card--transcript"); await expect(transcript).toContainText( "저는 익명 내담자이고 소속 기관에서 상담받고 있어요.", ); await expect(transcript).not.toContainText("[NAME]"); const summary = page.locator(".sr-summary"); await expect(summary).toContainText( "평가 AI는 익명 내담자가 소속 기관에서 보인 조심스러운 반응을 분석했습니다.", ); await expect(summary).not.toContainText("[NAME]"); await expect(summary).not.toContainText("[ORG]"); await openReviewTab(page, "워크시트"); await expect(page.locator(".sr-ws-evidence").filter({ hasText: "[NAME]" })).toBeVisible(); }); test("stores learner pre/post pilot evidence without effect claims", async ({ page }) => { await signInAsLearner(page); await routeFilledSessionReview(page); await routePrepostMeasures(page); await page.goto(`/learn/session/${FILLED_REVIEW_SESSION_ID}/review`); await expect(page.locator(".sr-overview")).toBeVisible({ timeout: 15_000 }); await openReviewTab(page, "피드백"); await expect(page.locator(".sr-card--prepost")).toBeVisible({ timeout: 15_000 }); const prepostInputs = page.locator(".sr-card--prepost input"); await expect(prepostInputs).toHaveCount(6); await expect(page.getByText("1/3 쌍")).toBeVisible(); await expect(page.getByText("효과 판정과 통계 검정은 평가설계 확정 후 별도 산출합니다.")).toBeVisible(); await prepostInputs.nth(2).fill("2.5"); await prepostInputs.nth(3).fill("3.5"); await expect(prepostInputs.nth(2)).toHaveValue("2.5"); await expect(prepostInputs.nth(3)).toHaveValue("3.5"); await page.getByRole("button", { name: "점수 저장" }).click(); await expect(page.getByText("2/3 쌍")).toBeVisible(); await expect(page.locator(".sr-prepost__actions")).toContainText("저장된 값 기준"); const prepostCard = page.locator(".sr-card--prepost"); await prepostInputs.nth(0).fill(""); await expect(prepostInputs.nth(0)).toHaveAttribute("aria-invalid", "true"); await expect(prepostCard.locator(".sr-prepost__actions")).toContainText("저장되지 않은 입력 있음"); await expect(prepostCard.locator(".sr-prepost__actions")).not.toContainText("저장된 값 기준"); await expect(prepostCard.getByRole("alert")).toContainText("기존 값을 비우려면 새 점수를 입력해 주세요."); await expect(prepostCard.getByRole("button", { name: "점수 저장" })).toBeDisabled(); }); });