모바일 메뉴와 주요 화면 UX 개선 및 시각 검수
This commit is contained in:
parent
89b5093ec7
commit
e8b770e4d9
50 changed files with 3189 additions and 457 deletions
137
apps/web/e2e/shell-navigation.spec.ts
Normal file
137
apps/web/e2e/shell-navigation.spec.ts
Normal 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$/);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue