import AxeBuilder from "@axe-core/playwright"; import { expect, test } from "@playwright/test"; import type { Locator, Page } from "@playwright/test"; 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 zoomTo200Percent(page: Page): Promise { await page.evaluate(() => { document.documentElement.style.zoom = "2"; }); } async function expectNoHorizontalOverflow(page: Page): Promise { const hasHorizontalOverflow = await page.evaluate( () => document.documentElement.scrollWidth > document.documentElement.clientWidth, ); expect(hasHorizontalOverflow).toBe(false); } async function expectBannerDoesNotOverlap(banner: Locator, control: Locator): Promise { const bannerBox = await banner.boundingBox(); const controlBox = await control.boundingBox(); expect(bannerBox).not.toBeNull(); expect(controlBox).not.toBeNull(); if (bannerBox === null || controlBox === null) { return; } expect(bannerBox.y + bannerBox.height).toBeLessThanOrEqual(controlBox.y); } test("logs in through mock mode and opens the protected shell without backend fallback", async ({ page }) => { // Given const interceptedApiContractRequests: string[] = []; page.on("request", (request) => { const url = new URL(request.url()); if (url.hostname !== "127.0.0.1" && url.pathname !== "/mockServiceWorker.js") { interceptedApiContractRequests.push(`${request.method()} ${url.pathname}${url.search}`); } }); // When await loginThroughMockMode(page); // Then await expect(page.getByRole("status", { name: /Mock Preview/ })).toBeVisible(); await expect(page.getByRole("heading", { name: "AI 캐릭터", exact: true })).toBeVisible(); expect(interceptedApiContractRequests).toEqual([ "POST /admin/member/login", "GET /api/v2/admin/ai-characters?page=0&size=20", ]); }); test("logs out and logs in again with a fresh mock preview session", async ({ page }) => { // Given await loginThroughMockMode(page); await expect(page.getByRole("heading", { name: "AI 캐릭터", exact: true })).toBeVisible(); // When await page.getByRole("button", { name: "로그아웃" }).click(); await expect(page).toHaveURL(/\/login$/); await page.getByLabel("이메일").fill("admin@test.com"); await page.getByLabel("비밀번호").fill("password"); await page.getByRole("button", { name: "로그인" }).click(); // Then await expect(page).toHaveURL(/\/ai-characters$/); await expect(page.getByRole("heading", { name: "AI 캐릭터", exact: true })).toBeVisible(); }); test("keeps the mock login banner and core controls usable at 320px and 200 percent zoom", async ({ page }) => { // Given await page.setViewportSize({ width: 320, height: 640 }); await page.goto("/login"); // When await zoomTo200Percent(page); // Then const banner = page.getByRole("status", { name: /Mock Preview/ }); const email = page.getByLabel("이메일"); await expect(banner).toBeVisible(); await expect(email).toBeVisible(); await expect(page.getByLabel("비밀번호")).toBeVisible(); await expect(page.getByRole("button", { name: "로그인" })).toBeVisible(); await expectBannerDoesNotOverlap(banner, email); await expectNoHorizontalOverflow(page); }); test("keeps the mock protected banner and shell controls usable at 320px and 200 percent zoom", async ({ page }) => { // Given await page.setViewportSize({ width: 320, height: 640 }); await loginThroughMockMode(page); // When await zoomTo200Percent(page); // Then const banner = page.getByRole("status", { name: /Mock Preview/ }); const menuButton = page.getByRole("button", { name: "모바일 메뉴 열기" }); await expect(banner).toBeVisible(); await expect(menuButton).toBeVisible(); await expect(page.getByRole("button", { name: "로그아웃" })).toBeVisible(); await expectBannerDoesNotOverlap(banner, menuButton); await expectNoHorizontalOverflow(page); }); test("keeps the mock banner in the mobile menu background and clears inert state at desktop widths", async ({ page }) => { // Given await loginThroughMockMode(page); for (const desktopWidth of [1024, 1200]) { await page.setViewportSize({ width: 320, height: 640 }); await page.getByRole("button", { name: "모바일 메뉴 열기" }).click(); const openState = await page.evaluate(() => { const banner = document.querySelector("[aria-label='Mock Preview']"); const main = document.querySelector("main[aria-label='AI 캐릭터 관리']"); return { bannerInert: banner?.closest("[inert]") !== null, bannerHidden: banner?.closest("[aria-hidden='true']") !== null, mainInert: main?.closest("[inert]") !== null, }; }); expect(openState).toEqual({ bannerHidden: true, bannerInert: true, mainInert: true }); // When await page.setViewportSize({ width: desktopWidth, height: 800 }); // Then await expect(page.getByRole("navigation", { name: "모바일 주 메뉴" })).toBeHidden(); await expect(page.getByRole("navigation", { name: "데스크톱 주 메뉴" })).toBeVisible(); await expect(page.getByRole("button", { name: "로그아웃" })).toBeVisible(); const desktopState = await page.evaluate(() => { const banner = document.querySelector("[aria-label='Mock Preview']"); const main = document.querySelector("main[aria-label='AI 캐릭터 관리']"); return { bannerInert: banner?.closest("[inert]") !== null, bannerHidden: banner?.closest("[aria-hidden='true']") !== null, mainInert: main?.closest("[inert]") !== null, }; }); expect(desktopState).toEqual({ bannerHidden: false, bannerInert: false, mainInert: false }); } }); test("has no critical or serious axe violations in mock preview mode", async ({ page }) => { // Given await loginThroughMockMode(page); // When const results = await new AxeBuilder({ page }).analyze(); const blockingViolations = results.violations.filter( (violation) => violation.impact === "critical" || violation.impact === "serious", ); // Then expect(blockingViolations).toEqual([]); });