세션 화면 디자인 개편

This commit is contained in:
Yun Chan 2026-07-31 09:19:42 +09:00
parent 04a5f485ed
commit 12bdc59497
13 changed files with 1430 additions and 386 deletions

View file

@ -673,7 +673,8 @@ test.describe("full sweep — counseling session", () => {
await routeSessionFixtureApi(page);
await startFixtureSession(page);
await expect(page.locator(".sx-signal__rest")).toContainText("아직 표시할 신호가 없어요");
await expect(page.locator(".sx-signal__rest")).toHaveCount(0);
await expect(page.locator(".sx-coach-card")).toBeVisible();
await page.getByLabel("학습자 발화 입력").fill(learnerText);
await page.getByRole("button", { name: "보내기" }).click();
@ -702,6 +703,7 @@ test.describe("full sweep — counseling session", () => {
test("shows the coach nudge for a pending turn and requests coaching immediately when opened", async ({
page,
}) => {
await page.setViewportSize({ width: 1366, height: 640 });
const api = await routeSessionFixtureApi(page);
await startFixtureSession(page);
@ -716,12 +718,175 @@ test.describe("full sweep — counseling session", () => {
expect(api.liveCoachRequests).toHaveLength(0);
await nudge.click();
await expect(page.locator(".sx-coach-card").getByText("감정 반영이 선명합니다")).toBeVisible();
const coachCard = page.locator(".sx-coach-card");
await expect(coachCard.getByText("감정 반영이 선명합니다")).toBeVisible();
await expect(page.locator(".sx-signal__wave")).toHaveCount(0);
const railLayout = await page.evaluate(() => {
const panel = document.querySelector<HTMLElement>(".sx-signal");
const coach = document.querySelector<HTMLElement>(".sx-coach-card");
const signal = document.querySelector<HTMLElement>(".sx-signal__one");
if (!panel || !coach || !signal) return null;
const panelRect = panel.getBoundingClientRect();
const coachRect = coach.getBoundingClientRect();
const signalRect = signal.getBoundingClientRect();
return {
coachBeforeSignal: coachRect.top < signalRect.top,
coachInsidePanel:
coachRect.top >= panelRect.top - 1 && coachRect.bottom <= panelRect.bottom + 1,
coachInsideViewport: coachRect.bottom <= window.innerHeight + 1,
};
});
expect(railLayout).toEqual({
coachBeforeSignal: true,
coachInsidePanel: true,
coachInsideViewport: true,
});
await page.setViewportSize({ width: 1024, height: 640 });
await expect(coachCard).toBeVisible();
await expect(page.locator(".sx-page--active .sx-col-right")).toBeVisible();
await expect(page.locator(".sx-signal__one")).toBeHidden();
const compactCoachInsideViewport = await coachCard.evaluate((element) => {
const rect = element.getBoundingClientRect();
return rect.top >= -1 && rect.bottom <= window.innerHeight + 1;
});
expect(compactCoachInsideViewport).toBe(true);
await expect(nudge).toHaveCount(0);
expect(api.liveCoachRequests).toHaveLength(1);
expect(api.liveCoachRequests[0]).toMatchObject({ learner_text: learnerText });
});
test("uses the global light theme tokens throughout an active session", async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem("vignette.theme", "light");
});
await routeSessionFixtureApi(page);
await startFixtureSession(page);
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await page.getByRole("button", { name: /코칭 모드, 남은 기회/ }).click();
await expect(page.locator(".sx-coach-bubble")).toBeVisible();
const themeState = await page.evaluate(() => {
const rootStyle = getComputedStyle(document.documentElement);
const session = document.querySelector<HTMLElement>(".sx-page--active");
const coachBubble = document.querySelector<HTMLElement>(".sx-coach-bubble");
if (!session || !coachBubble) return null;
const sessionStyle = getComputedStyle(session);
const coachStyle = getComputedStyle(coachBubble);
return {
rootSurface: rootStyle.getPropertyValue("--bg-surface").trim(),
sessionSurface: sessionStyle.getPropertyValue("--bg-surface").trim(),
rootText: rootStyle.getPropertyValue("--text-strong").trim(),
sessionText: sessionStyle.getPropertyValue("--text-strong").trim(),
colorScheme: sessionStyle.colorScheme,
coachInsetSurface:
coachBubble.classList.contains("vg-surface--inset") &&
coachStyle.getPropertyValue("--glass-surface-inset").trim() ===
rootStyle.getPropertyValue("--glass-surface-inset").trim(),
};
});
expect(themeState).not.toBeNull();
expect(themeState!.sessionSurface).toBe(themeState!.rootSurface);
expect(themeState!.sessionText).toBe(themeState!.rootText);
expect(themeState!.colorScheme).toBe("light");
expect(themeState!.coachInsetSurface).toBe(true);
});
test("matches the 1536px botanical session workspace composition", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1536, height: 1024 });
await page.addInitScript(() => {
localStorage.setItem("vignette.theme", "light");
});
await routeSessionFixtureApi(page);
await startFixtureSession(page);
await expect(page.getByText("라이브 코칭", { exact: true })).toBeVisible();
await expect(page.locator(".sx-coach-card")).toBeVisible();
await expect(page.getByRole("button", { name: "상태 신호" })).toHaveClass(/is-on/);
const layout = await page.evaluate(async () => {
const pageRoot = document.querySelector<HTMLElement>(".sx-page--active");
const bar = document.querySelector<HTMLElement>(".sx-sessionbar");
const grid = document.querySelector<HTMLElement>(".sx-grid");
const left = document.querySelector<HTMLElement>(".sx-col-left");
const center = document.querySelector<HTMLElement>(".sx-col-center");
const right = document.querySelector<HTMLElement>(".sx-col-right");
const controls = document.querySelector<HTMLElement>(".sx-controlbar");
const mic = document.querySelector<HTMLElement>(".sx-mic");
const micText = document.querySelector<HTMLElement>(".sx-mic-block__ms");
if (!pageRoot || !bar || !grid || !left || !center || !right || !controls || !mic || !micText) {
return null;
}
const rect = (element: HTMLElement) => {
const value = element.getBoundingClientRect();
return {
left: Math.round(value.left),
top: Math.round(value.top),
right: Math.round(value.right),
bottom: Math.round(value.bottom),
width: Math.round(value.width),
height: Math.round(value.height),
};
};
const leafResponse = await fetch("/session-botanical/leaf-1.webp");
return {
viewport: { width: window.innerWidth, height: window.innerHeight },
page: rect(pageRoot),
bar: rect(bar),
grid: rect(grid),
left: rect(left),
center: rect(center),
right: rect(right),
controls: rect(controls),
mic: rect(mic),
micText: rect(micText),
pageLeaf: getComputedStyle(pageRoot, "::before").backgroundImage,
stageLeaf: getComputedStyle(
document.querySelector<HTMLElement>(".sx-orb-wrap")!,
"::before",
).backgroundImage,
leafAsset: {
ok: leafResponse.ok,
contentType: leafResponse.headers.get("content-type"),
},
};
});
expect(layout).not.toBeNull();
expect(layout!.viewport).toEqual({ width: 1536, height: 1024 });
expect(layout!.page).toMatchObject({ left: 0, top: 0, width: 1536, height: 1024 });
expect(layout!.bar.width).toBeGreaterThanOrEqual(1400);
expect(layout!.bar.width).toBeLessThanOrEqual(1410);
expect(layout!.grid.width).toBeGreaterThanOrEqual(1400);
expect(layout!.grid.width).toBeLessThanOrEqual(1410);
expect(layout!.left.width).toBeGreaterThanOrEqual(324);
expect(layout!.left.width).toBeLessThanOrEqual(328);
expect(layout!.right.width).toBeGreaterThanOrEqual(355);
expect(layout!.right.width).toBeLessThanOrEqual(359);
expect(layout!.center.left).toBeGreaterThan(layout!.left.right);
expect(layout!.right.left).toBeGreaterThan(layout!.center.right);
expect(layout!.controls.width).toBeGreaterThanOrEqual(1460);
expect(layout!.controls.width).toBeLessThanOrEqual(1470);
expect(layout!.controls.bottom).toBeLessThanOrEqual(1024);
expect(layout!.micText.left).toBeGreaterThanOrEqual(layout!.mic.right + 8);
expect(layout!.pageLeaf).toContain("leaf-5.webp");
expect(layout!.stageLeaf).toContain("leaf-1.webp");
expect(layout!.leafAsset).toEqual({ ok: true, contentType: "image/webp" });
const screenshotPath = testInfo.outputPath("session-botanical-1536x1024.png");
await page.screenshot({ path: screenshotPath, animations: "disabled" });
await testInfo.attach("session-botanical-1536x1024", {
path: screenshotPath,
contentType: "image/png",
});
});
// checklist: session-meters-collapse
// 모바일(≤880px) 압축 레이아웃은 관찰 게이지 패널을 의도적으로 숨긴다(session.css) —
// 데스크톱 전용 UI라 desktop 프로젝트에서만 검증한다.