import { promises as fs } from "node:fs"; import path from "node:path"; import { expect, test, type Page } from "@playwright/test"; import { expectNoHorizontalOverflow } from "./support"; const SHOT_DIR = path.join(process.cwd(), "node_modules", ".tmp", "auth-visual"); const THEMES = ["light", "dark"] as const; const VIEWPORTS = [ { width: 390, height: 844, label: "mobile" }, { width: 1280, height: 800, label: "desktop" }, ] as const; async function setTheme(page: Page, theme: (typeof THEMES)[number], pathName: string) { await page.goto(pathName); await page.evaluate((nextTheme) => { localStorage.setItem("vignette.theme", nextTheme); }, theme); await page.reload(); } async function expectGlassSurface(page: Page, selector: string) { const surface = await page.locator(selector).evaluate((element) => { const style = getComputedStyle(element); return { backgroundImage: style.backgroundImage, borderColor: style.borderColor, boxShadow: style.boxShadow, }; }); expect(surface.backgroundImage.match(/linear-gradient/g)?.length ?? 0).toBeGreaterThanOrEqual(1); expect(surface.borderColor).not.toBe("rgba(0, 0, 0, 0)"); expect(surface.boxShadow).not.toBe("none"); } test("@single-run 인증 화면이 공통 테마와 전체 viewport를 유지한다", async ({ page }) => { await fs.mkdir(SHOT_DIR, { recursive: true }); const loginRoomBackgrounds = new Map<(typeof THEMES)[number], string>(); for (const theme of THEMES) { for (const viewport of VIEWPORTS) { await page.setViewportSize(viewport); await setTheme(page, theme, "/login"); await expect(page.locator(".lg-panel")).toBeVisible(); await expect(page.locator("html")).toHaveAttribute("data-theme", theme); await expectNoHorizontalOverflow(page); await expectGlassSurface(page, ".lg-panel"); const themeColors = await page.locator(".lg-root").evaluate((root) => { const brand = root.querySelector(".lg-brand"); const panel = root.querySelector(".lg-panel"); const shellStyle = getComputedStyle(root); if (!brand || !panel) throw new Error("login theme surfaces are missing"); const luma = (color: string) => { const [red = 0, green = 0, blue = 0] = color.match(/\d+(?:\.\d+)?/g)?.map(Number) ?? []; return red * 0.2126 + green * 0.7152 + blue * 0.0722; }; return { brandLuma: luma(getComputedStyle(brand).color), panelLuma: luma(getComputedStyle(panel).color), roomBackground: shellStyle.backgroundImage, }; }); if (theme === "light") { expect(themeColors.brandLuma).toBeLessThan(96); expect(themeColors.panelLuma).toBeLessThan(96); } else { expect(themeColors.brandLuma).toBeGreaterThan(180); expect(themeColors.panelLuma).toBeGreaterThan(180); } loginRoomBackgrounds.set(theme, themeColors.roomBackground); const canvas = await page.locator(".vg-auth-shell").evaluate((element) => { const rect = element.getBoundingClientRect(); return { width: Math.round(rect.width), minHeight: Math.round(rect.height), viewportWidth: document.documentElement.clientWidth, viewportHeight: window.innerHeight, }; }); expect(canvas.width).toBe(canvas.viewportWidth); expect(canvas.minHeight).toBeGreaterThanOrEqual(canvas.viewportHeight); await page.screenshot({ path: path.join(SHOT_DIR, `login-${theme}-${viewport.label}.png`), fullPage: true, }); } } expect(loginRoomBackgrounds.get("light")).not.toBe(loginRoomBackgrounds.get("dark")); const login = await page.request.post("/api/auth/dev-login", { data: { email: `auth-visual.${Date.now()}@hs.ac.kr`, role: "learner", display_name: "인증 화면 검증 학습자", }, }); expect(login.ok(), await login.text()).toBeTruthy(); for (const theme of THEMES) { for (const viewport of VIEWPORTS) { await page.setViewportSize(viewport); await setTheme(page, theme, "/onboarding"); await expect(page.getByRole("heading", { name: "가입 정보를 입력합니다." })).toBeVisible(); await expect(page.locator("html")).toHaveAttribute("data-theme", theme); await expectNoHorizontalOverflow(page); await expectGlassSurface(page, ".ob-shell"); await page.screenshot({ path: path.join(SHOT_DIR, `onboarding-${theme}-${viewport.label}.png`), fullPage: true, }); } } });