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$/); }); });