모바일 메뉴와 주요 화면 UX 개선 및 시각 검수

This commit is contained in:
Yun Chan 2026-09-12 17:57:59 +09:00
parent 89b5093ec7
commit e8b770e4d9
50 changed files with 3189 additions and 457 deletions

View file

@ -0,0 +1,137 @@
import { expect, test, type Page } from "@playwright/test";
import {
expectNoHorizontalOverflow,
signInAsAdmin,
signInAsLearner,
signInAsTeacher,
} from "./support";
const viewports = [
{ width: 320, height: 568 },
{ width: 390, height: 844 },
{ width: 768, height: 900 },
{ width: 1024, height: 900 },
];
async function expectMinimumTargets(page: Page, selector: string) {
const targets = await page.locator(selector).evaluateAll((elements) =>
elements
.filter((element) => window.getComputedStyle(element).display !== "none")
.map((element) => {
const rect = element.getBoundingClientRect();
return { text: element.textContent?.trim(), width: rect.width, height: rect.height };
}),
);
for (const target of targets) {
expect(target.width, `${target.text} width`).toBeGreaterThanOrEqual(44);
expect(target.height, `${target.text} height`).toBeGreaterThanOrEqual(44);
}
}
test.describe("공통 셸 내비게이션", () => {
test("역할별 하단 목적지와 태블릿 레일은 레이블·조작면·오버플로 계약을 지킨다", async ({ page }) => {
const roles = [
{
signIn: signInAsLearner,
path: "/learn",
mobile: ["학습자 홈", "상담 연습", "학습 기록"],
desktop: ["학습자 홈", "상담 연습", "학습 기록", "설정"],
},
{
signIn: signInAsTeacher,
path: "/teach",
mobile: ["교수자 홈", "학생 분석", "감독·연구"],
desktop: ["교수자 홈", "학생 분석", "감독·연구", "내담자 설정", "설정"],
},
{
signIn: signInAsAdmin,
path: "/admin",
mobile: ["운영 홈", "AI 운영", "사용자 관리"],
desktop: ["운영 홈", "AI 운영", "사용자 관리", "접근 관리", "지원 요청"],
},
] as const;
for (const role of roles) {
await role.signIn(page);
await page.goto(role.path);
await expect(page.locator(".vg-shell")).toBeVisible({ timeout: 15_000 });
for (const viewport of viewports) {
await page.setViewportSize(viewport);
await expectNoHorizontalOverflow(page);
if (viewport.width <= 720) {
const labels = await page.locator(".vg-nav__item--mobile-primary:visible .vg-nav__txt").allTextContents();
expect(labels).toEqual(role.mobile);
await expect(page.getByRole("button", { name: "전체 메뉴" })).toBeVisible();
await expectMinimumTargets(page, ".vg-nav__item--mobile-primary:visible, .vg-nav__more:visible");
} else {
for (const label of role.desktop) {
await expect(page.getByRole("link", { name: label, exact: true })).toBeVisible();
}
if (viewport.width === 1024 && role.path === "/admin") {
for (const group of ["운영", "교수", "학습", "설정"]) {
await expect(page.locator(".vg-nav__label", { hasText: group })).toBeVisible();
}
}
}
}
}
});
test("전체 메뉴는 키보드로 닫히고 현재 위치를 보존하며 교수자 홈 단축키를 정확히 보낸다", async ({ page }) => {
await signInAsTeacher(page);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/teach/personas");
await expect(page.locator(".vg-shell")).toBeVisible({ timeout: 15_000 });
const menuButton = page.getByRole("button", { name: "전체 메뉴" });
await expect(menuButton).toHaveClass(/is-active/);
await menuButton.click();
const dialog = page.locator("#vg-all-menu-dialog");
await expect(dialog).toBeVisible();
await expect(dialog.getByRole("heading", { name: "전체 메뉴" })).toBeVisible();
await expect(dialog.getByRole("link", { name: "학생 분석" })).toBeVisible();
await expect(page.getByRole("button", { name: "전체 메뉴 닫기" })).toBeFocused();
await page.keyboard.press("Escape");
await expect(dialog).not.toBeVisible();
await expect(menuButton).toBeFocused();
await menuButton.click();
await dialog.getByRole("link", { name: "학생 분석" }).click();
await expect(page).toHaveURL(/\/teach\/analysis$/);
await expect(dialog).not.toBeVisible();
await page.goto("/teach/analysis", { waitUntil: "networkidle" });
await page.keyboard.press("Alt+h");
await expect(page).toHaveURL(/\/teach$/);
const browserShortcutCancellation = await page.evaluate(() => {
const results = ["t", "w"].map((key) => {
const event = new KeyboardEvent("keydown", { key, ctrlKey: true, bubbles: true, cancelable: true });
window.dispatchEvent(event);
return event.defaultPrevented;
});
return results;
});
expect(browserShortcutCancellation).toEqual([false, false]);
});
test("좁은 화면의 공간 선택은 레이블을 유지하며 44px 조작면을 지킨다", async ({ page }) => {
await signInAsAdmin(page);
await page.setViewportSize({ width: 320, height: 568 });
await page.goto("/admin");
await expect(page.locator(".vg-shell")).toBeVisible({ timeout: 15_000 });
const select = page.getByRole("combobox", { name: "공간 선택" });
await expect(select).toBeVisible();
await expect(select).toHaveValue("admin");
await expect(select.locator("option")).toHaveText(["학습자", "교수자", "관리자"]);
await expectMinimumTargets(page, ".vg-topbar__switch-select:visible, .vg-iconbtn:visible");
await select.selectOption("teacher");
await expect(page).toHaveURL(/\/teach$/);
});
});