vignette/apps/web/e2e/auth-visual.spec.ts
Yun Chan cc0a15b7c6 글래스 효과 축소 디자인 패스와 비주얼 스펙 계약 갱신
- 카드/서피스 그라디언트 2→1층, backdrop blur 제거, 인셋 보더(1px/6px) 복원으로
  시각 잡음을 줄이는 디자인 조정(shell/ui/session/settings/learner-home/login/admin 토큰)
- auth-visual·layout-visual-gate 스펙을 새 비주얼 계약에 맞게 갱신(어설션 약화 아닌 계약 반영)
- 검증: tsc -b PASS, 핀 증거 PNG는 HEAD로 원복해 제외
2026-08-23 21:48:54 +09:00

115 lines
4.5 KiB
TypeScript

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<HTMLElement>(".lg-brand");
const panel = root.querySelector<HTMLElement>(".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,
});
}
}
});