import { expect, test } from "@playwright/test"; import type { Locator, Page } from "@playwright/test"; type ViewportCase = { readonly height: number; readonly name: string; readonly width: number; }; const viewports = [ { height: 640, name: "mobile 320", width: 320 }, { height: 640, name: "mobile 640", width: 640 }, { height: 900, name: "tablet 768", width: 768 }, { height: 900, name: "desktop 1024", width: 1024 }, { height: 900, name: "desktop 1280", width: 1280 }, { height: 390, name: "mobile landscape", width: 844 }, ] as const satisfies readonly ViewportCase[]; async function loginThroughMockMode(page: Page): Promise { await page.goto("/login"); await page.getByLabel("이메일").fill("admin@test.com"); await page.getByLabel("비밀번호").fill("password"); await page.getByRole("button", { name: "로그인" }).click(); await expect(page).toHaveURL(/\/ai-characters$/); } async function expectNoHorizontalOverflow(page: Page): Promise { expect(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth)).toBe(false); } async function expectTouchTarget(locator: Locator): Promise { const box = await locator.boundingBox(); expect(box).not.toBeNull(); expect(box?.height ?? 0).toBeGreaterThanOrEqual(44); } test("mock shell stays usable across the P9 viewport matrix", async ({ page }) => { // Given await loginThroughMockMode(page); for (const viewport of viewports) { // When await page.setViewportSize(viewport); await page.goto("/ai-characters"); // Then await expect(page.getByRole("main", { name: "AI 캐릭터 관리" })).toBeVisible(); await expect(page.getByRole("status", { name: /Mock Preview/ })).toBeVisible(); await expectNoHorizontalOverflow(page); if (viewport.width < 768) { await expectTouchTarget(page.getByRole("button", { name: "모바일 메뉴 열기" })); await expectTouchTarget(page.getByRole("button", { name: "로그아웃" })); } } }); test("route capability matches mobile read-only and tablet desktop management rules", async ({ page }) => { // Given await loginThroughMockMode(page); // When / Then await page.setViewportSize({ width: 320, height: 640 }); await page.goto("/ai-characters/101/audio-contents/new"); await expect(page.getByText(/데스크톱.*(생성|업로드|이용)/)).toBeVisible(); await expect(page.getByRole("button", { name: "생성" })).toBeHidden(); await page.goto("/ai-characters/101/community-posts/new"); await expect(page.getByRole("heading", { name: "모바일에서는 커뮤니티 게시글 생성을 제한합니다" })).toBeVisible(); await expect(page.getByRole("button", { name: "생성" })).toBeHidden(); await page.goto("/ai-characters/101/fan-talks"); await expect(page.getByRole("button", { name: "첫 번째 응원입니다. 답변하기" })).toBeVisible(); await page.setViewportSize({ width: 768, height: 900 }); await page.goto("/ai-characters/101/community-posts"); await expect(page.getByRole("link", { name: "커뮤니티 게시글 생성" })).toBeVisible(); await page.setViewportSize({ width: 1280, height: 900 }); await page.goto("/ai-characters/101/series/5001"); await expect(page.getByRole("searchbox", { name: "검색어" })).toBeVisible(); await expectNoHorizontalOverflow(page); });