학생 회기 모바일 사용성 보정

This commit is contained in:
Yun Chan 2026-08-09 22:00:32 +09:00
parent aaebe4450e
commit c743e9ccb9
7 changed files with 395 additions and 31 deletions

View file

@ -691,6 +691,69 @@ test.describe("full sweep — counseling session", () => {
await expect(transcript).toContainText("소속 기관에서 오라고 했어요");
});
test("caps an overdue active session honestly and preserves 44px mobile controls", async ({
page,
}) => {
await routeSessionFixtureApi(page, {
durationLimitSeconds: 3600,
detailOverrides: {
started_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
},
});
await page.goto(`/learn/session/${fixtureSessionId}`);
await expect(page.locator("#sx-end-dialog-title")).toHaveText(
"회기 시간이 끝났어요",
);
await expect(page.locator(".sx-sessionbar__meta")).toContainText("시간 만료");
await expect(page.locator("span.sr-only[aria-live='polite']")).toHaveText(
"회기 시간 만료",
);
for (const viewport of [
{ width: 390, height: 844 },
{ width: 320, height: 568 },
]) {
await page.setViewportSize(viewport);
await page.evaluate(() => new Promise(requestAnimationFrame));
const targetMetrics = await page
.locator(".sx-page--active button:visible, .sx-page--active textarea:visible")
.evaluateAll((elements) =>
elements.map((element) => {
const rect = element.getBoundingClientRect();
return {
label:
element.getAttribute("aria-label") ||
element.textContent?.replace(/\s+/g, " ").trim() ||
element.tagName,
width: rect.width,
height: rect.height,
};
}),
);
expect(targetMetrics.length, `${viewport.width} visible controls`).toBeGreaterThan(0);
for (const target of targetMetrics) {
expect(
target.width,
`${viewport.width} ${target.label} touch width`,
).toBeGreaterThanOrEqual(44);
expect(
target.height,
`${viewport.width} ${target.label} touch height`,
).toBeGreaterThanOrEqual(44);
}
const layout = await page.evaluate(() => ({
viewportWidth: window.innerWidth,
documentWidth: document.documentElement.scrollWidth,
bodyText: document.body.innerText,
}));
expect(layout.documentWidth, `${viewport.width} horizontal overflow`).toBeLessThanOrEqual(
layout.viewportWidth,
);
expect(layout.bodyText).not.toMatch(/\b\d{4,}:\d{2}\b/);
}
});
// checklist: session-transcript-autoscroll
test("releases autoscroll when scrolling up, jumps back with the latest button, and follows new turns", async ({
page,
@ -715,12 +778,27 @@ test.describe("full sweep — counseling session", () => {
const jumpButton = page.getByRole("button", { name: "최신으로" });
await expect(jumpButton).toHaveCount(0);
// 최신 발화를 따라가는 동안 scrollport가 모바일 레이아웃으로 재배치돼도
// ResizeObserver가 새 하단에 붙이고 layout scroll을 사용자 이탈로 오인하지 않는다.
await page.setViewportSize({ width: 390, height: 844 });
await expect
.poll(() =>
scroller.evaluate((el) => el.scrollHeight - el.scrollTop - el.clientHeight),
)
.toBeLessThan(24);
await expect(jumpButton).toHaveCount(0);
// 위로 스크롤 → 자동 따라가기 해제 + '최신으로' 복귀 버튼 노출.
await scroller.evaluate((el) => {
el.scrollTop = 0;
el.dispatchEvent(new Event("scroll"));
});
await scroller.hover();
await page.mouse.wheel(0, -10_000);
await expect(jumpButton).toBeVisible();
await page.setViewportSize({ width: 320, height: 568 });
await expect(jumpButton).toBeVisible();
await expect
.poll(() =>
scroller.evaluate((el) => el.scrollHeight - el.scrollTop - el.clientHeight),
)
.toBeGreaterThanOrEqual(24);
await jumpButton.click();
await expect(jumpButton).toHaveCount(0);