feat: 운영 안정성과 세션 음성 경험 개선

This commit is contained in:
Yun Chan 2026-07-31 00:13:08 +09:00
parent facc4ad2d9
commit c788343467
95 changed files with 8431 additions and 1785 deletions

View file

@ -1,4 +1,4 @@
import { expect, test, type APIResponse, type Page, type Response, type TestInfo } from "@playwright/test";
import { expect, test, type APIResponse, type Page, type Response, type TestInfo } from "@playwright/test";
import {
completeOnboarding,
expectNoHorizontalOverflow,
@ -99,6 +99,7 @@ interface AdminEngineConfigResponse {
engine_mode: string;
engine_url: string;
model: string;
reasoning_effort: string | null;
source: "database" | "runtime_cache" | "runtime_default";
durable: boolean;
updated_by: string | null;
@ -174,9 +175,11 @@ async function mockAdminSession(
adminTickets?: unknown;
adminUsage?: AdminUsageResponse;
engineConfig?: AdminEngineConfigResponse;
authFailuresBeforeSuccess?: number;
} = {},
) {
const seenAdminEndpoints = new Set<string>();
let authAttempts = 0;
const json = (body: unknown) => JSON.stringify(body);
await page.route("**/api/**", async (route) => {
@ -192,6 +195,11 @@ async function mockAdminSession(
});
if (method === "GET" && path.endsWith("/auth/me")) {
authAttempts += 1;
if (authAttempts <= (options.authFailuresBeforeSuccess ?? 0)) {
await fulfillJson({ detail: "public runtime is starting" }, 503);
return;
}
await fulfillJson({
user_id: authUser.user_id ?? "stale-admin",
email: authUser.email ?? "stale-admin@twentyoz.kr",
@ -268,6 +276,64 @@ async function mockAdminSession(
return;
}
if (method === "GET" && path.endsWith("/admin/engine-capabilities")) {
seenAdminEndpoints.add("engine-capabilities");
const provider = url.searchParams.get("engine_mode") ?? "openai";
const models =
provider === "codex_cli"
? [
{
id: "gpt-5.6-terra",
label: "GPT-5.6 Terra",
description: "Codex 기본 모델",
reasoning_efforts: ["low", "medium", "high"],
default_reasoning_effort: "medium",
is_default: true,
},
]
: provider === "agy_cli"
? [
{
id: "gemini-3.6-flash-high",
label: "Gemini 3.6 Flash (High)",
description: "Agy 기본 모델",
reasoning_efforts: ["high"],
default_reasoning_effort: "high",
is_default: true,
},
]
: [
{
id: "gateway-default",
label: "게이트웨이 기본 모델",
description: "테스트 기본 모델",
reasoning_efforts: ["medium"],
default_reasoning_effort: "medium",
is_default: true,
},
{
id: "gpt-5.1-mini",
label: "GPT-5.1 Mini",
description: "테스트 선택 모델",
reasoning_efforts: ["low", "medium", "high"],
default_reasoning_effort: "medium",
is_default: false,
},
];
const defaultModel = models[0];
await fulfillJson({
provider,
available: true,
source: "live_cli",
models,
default_model: defaultModel.id,
default_reasoning_effort: defaultModel.default_reasoning_effort,
detail: "테스트 모델 목록",
fetched_at: 1_783_990_800,
});
return;
}
if (method === "GET" && path.endsWith("/admin/engine-config")) {
seenAdminEndpoints.add("engine-config");
await fulfillJson(
@ -275,6 +341,7 @@ async function mockAdminSession(
engine_mode: "openai",
engine_url: "http://127.0.0.1:9099",
model: "gateway-default",
reasoning_effort: "medium",
source: "database",
durable: true,
updated_by: "admin@twentyoz.kr",
@ -292,6 +359,7 @@ async function mockAdminSession(
engine_mode: "openai",
engine_url: "http://127.0.0.1:9099",
model: "gateway-default",
reasoning_effort: "medium",
}),
...body,
source: "database",
@ -565,7 +633,7 @@ async function openAdminTickets(page: Page) {
}
async function expectCreateUserControlsFit(page: Page, viewportWidth: number) {
const form = page.locator(".ad-user-create");
const form = page.locator(".vgops-user-create");
await expect(form).toBeVisible();
const clippedControls = await form.evaluate((element) => {
@ -619,7 +687,7 @@ async function expectVisibleButtonsFit(page: Page, selector: string, context: st
.map((button) => {
const rect = button.getBoundingClientRect();
const owner =
button.closest<HTMLElement>(".ad-user,.ad-user-create,.ad-user-table tbody tr") ??
button.closest<HTMLElement>(".vgops-user,.vgops-user-create,.vgops-user-table tbody tr") ??
button.parentElement;
const ownerRect = owner?.getBoundingClientRect();
const style = window.getComputedStyle(button);
@ -661,7 +729,7 @@ test.describe("admin route guards", () => {
await expect(page).toHaveURL(/\/admin$/);
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
await expect(page.locator(".ad-status")).toContainText("개발");
await expect(page.locator(".vgops-status")).toContainText("개발");
await expect
.poll(() => Array.from(seenAdminEndpoints).sort())
.toEqual(["health", "tickets", "uptime", "usage", "users"]);
@ -688,6 +756,118 @@ test.describe("admin route guards", () => {
.toEqual(["health", "tickets", "uptime", "usage", "users"]);
});
test("restores every admin page for admin, super-admin, and delegated admin sessions", async ({
page,
}) => {
const adminRoutes = [
{ path: "/admin", heading: "현재 서비스 상태" },
{ path: "/admin/ai", heading: "AI 운영과 DB 계량" },
{ path: "/admin/users", heading: "가입 승인과 권한 관리" },
{ path: "/admin/access", heading: "역할, 그룹, 접근 범위" },
{ path: "/admin/tickets", heading: "사용자 문제 큐" },
] as const;
const sessions = [
{
user_id: "role-admin",
email: "role-admin@twentyoz.kr",
display_name: "Role Admin",
role: "admin" as const,
admin_access: false,
super_admin: false,
},
{
user_id: "super-admin",
email: "super-admin@twentyoz.kr",
display_name: "Super Admin",
role: "learner" as const,
admin_access: false,
super_admin: true,
},
{
user_id: "delegated-admin",
email: "delegated-admin@hs.ac.kr",
display_name: "Delegated Admin",
role: "teacher" as const,
admin_access: true,
super_admin: false,
},
] as const;
for (const session of sessions) {
await page.unrouteAll({ behavior: "wait" });
await mockAdminSession(page, {
...session,
onboarding_completed_at: 1_782_900_000,
});
for (const route of adminRoutes) {
await page.goto(route.path);
await expect(page).toHaveURL(new RegExp(`${route.path.replace("/", "\\/")}$`));
await expect(page.getByRole("heading", { name: route.heading })).toBeVisible();
await expect(page.locator(".vg-nav").getByRole("link", { name: "운영 홈" })).toBeVisible();
await expect(page.locator(".vg-nav").getByRole("link", { name: "AI 운영" })).toBeVisible();
await expect(page.locator(".vg-nav").getByRole("link", { name: "사용자" })).toBeVisible();
await expect(page.locator(".vg-nav").getByRole("link", { name: "권한" })).toBeVisible();
await expect(page.locator(".vg-nav").getByRole("link", { name: "티켓" })).toBeVisible();
}
}
});
test("keeps the admin entry visible after a primary-role super-admin leaves the admin workspace", async ({
page,
}) => {
await mockAdminSession(page, {
user_id: "primary-role-super-admin",
email: "primary-role-super-admin@twentyoz.kr",
display_name: "Primary Role Super Admin",
role: "learner",
admin_access: false,
super_admin: true,
onboarding_completed_at: 1_782_900_000,
});
await page.goto("/admin");
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
await page.locator(".vg-nav").getByRole("link", { name: "학습자 홈" }).click();
await expect(page).toHaveURL(/\/learn$/);
await expect(page.locator(".vg-nav").getByRole("link", { name: "운영 콘솔" })).toBeVisible();
await page.locator(".vg-nav").getByRole("link", { name: "운영 콘솔" }).click();
await expect(page).toHaveURL(/\/admin$/);
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
});
test("keeps the session recoverable when auth restore starts during a server restart", async ({
page,
}) => {
await mockAdminSession(
page,
{
user_id: "restart-admin",
email: "restart-admin@twentyoz.kr",
display_name: "Restart Admin",
role: "admin",
admin_access: true,
super_admin: true,
onboarding_completed_at: 1_782_900_000,
},
{ authFailuresBeforeSuccess: 3 },
);
await page.goto("/admin");
await expect(page.getByTestId("auth-restore-failed")).toBeVisible();
await expect(page.getByRole("heading", { name: "관리자 권한이 사라진 것이 아닙니다." })).toBeVisible();
await expect(page.locator("#vignette-boot-diagnostic")).toHaveCount(0);
await page.getByTestId("auth-restore-retry").click();
await expect(page).toHaveURL(/\/admin$/);
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
await expect(page.getByTestId("auth-restore-failed")).toHaveCount(0);
});
test("shows detailed AI metering and saves the real engine configuration", async ({ page }) => {
const seenAdminEndpoints = await mockAdminSession(
page,
@ -755,11 +935,14 @@ test.describe("admin route guards", () => {
"page",
);
await expect(page.getByText("운영 DB 원장").first()).toBeVisible();
await expect(page.locator(".aic-ledger")).toContainText("$6.6212");
// 2026-07-27 D7: 합계 금액은 화면에 소수 2자리로 표시하고 원본 정밀도는 title 로 옮겼다.
await expect(page.locator(".aic-ledger")).toContainText("$6.62");
await expect(page.locator(".aic-ledger b").first()).toHaveAttribute("title", /6\.6212/);
await expect(page.locator(".aic-budget")).toContainText("93.8%");
await expect(page.locator(".aic-table")).toContainText("gpt-5-mini");
await expect(page.locator(".aic-cache-score")).toContainText("80%");
await expect(page.getByLabel("AI 기본 모델")).toHaveValue("gateway-default");
await expect(page.getByLabel("AI 엔진 공급자").locator("option")).toHaveCount(6);
const usageRequest = page.waitForRequest((request) =>
request.url().includes("/api/admin/usage?window_days=7"),
@ -767,17 +950,40 @@ test.describe("admin route guards", () => {
await page.getByRole("button", { name: "7일" }).click();
await usageRequest;
await page.getByLabel("AI 기본 모델").fill("gpt-5.1-mini");
await page.getByLabel("AI 엔진 공급자").selectOption("codex_cli");
await expect(page.getByLabel("AI 기본 모델")).toHaveValue("gpt-5.6-terra");
await expect(page.getByLabel("AI 추론 강도")).toHaveValue("medium");
await page.getByLabel("AI 엔진 공급자").selectOption("agy_cli");
await expect(page.getByLabel("AI 기본 모델")).toHaveValue("gemini-3.6-flash-high");
await expect(page.getByLabel("AI 추론 강도")).toHaveValue("high");
await page.getByLabel("AI 연결 주소").fill("http://127.0.0.1:9199");
await expect(page.getByRole("button", { name: "운영 설정 저장" })).toBeDisabled();
const catalogRequest = page.waitForRequest((request) => {
const url = new URL(request.url());
return (
url.pathname.endsWith("/api/admin/engine-capabilities") &&
url.searchParams.get("engine_url") === "http://127.0.0.1:9199"
);
});
await page.getByRole("button", { name: "목록 새로고침" }).click();
await catalogRequest;
await expect(page.getByLabel("AI 기본 모델")).toHaveValue("gemini-3.6-flash-high");
const patchRequest = page.waitForRequest((request) =>
request.method() === "PATCH" && request.url().endsWith("/api/admin/engine-config"),
);
await page.getByRole("button", { name: "운영 설정 저장" }).click();
const request = await patchRequest;
expect(request.postDataJSON()).toMatchObject({ model: "gpt-5.1-mini" });
expect(request.postDataJSON()).toMatchObject({
engine_mode: "agy_cli",
engine_url: "http://127.0.0.1:9199",
model: "gemini-3.6-flash-high",
reasoning_effort: "high",
});
await expect(page.getByText("저장됨")).toBeVisible();
await expect
.poll(() => Array.from(seenAdminEndpoints).sort())
.toEqual(["engine-config", "engine-config-patch", "health", "usage"]);
.toEqual(["engine-capabilities", "engine-config", "engine-config-patch", "health", "usage"]);
await expectNoHorizontalOverflow(page);
});
@ -817,10 +1023,12 @@ test.describe("admin route guards", () => {
await page.goto("/admin/users");
await expect(page.getByRole("heading", { name: "가입 승인과 권한 관리" })).toBeVisible();
const scrolled = await page.evaluate(() => {
document.documentElement.style.minHeight = "2200px";
document.body.style.minHeight = "2200px";
window.scrollTo(0, 900);
return window.scrollY;
const main = document.querySelector<HTMLElement>(".vg-main");
const root = document.querySelector<HTMLElement>(".vgops-root");
if (!main || !root) throw new Error("admin scroll container missing");
root.style.minHeight = "2200px";
main.scrollTop = 900;
return main.scrollTop;
});
expect(scrolled).toBeGreaterThan(0);
@ -828,9 +1036,13 @@ test.describe("admin route guards", () => {
await expect(page).toHaveURL(/\/admin\/access$/);
await expect
.poll(() => page.evaluate(() => window.scrollY))
.poll(() =>
page.evaluate(
() => document.querySelector<HTMLElement>(".vg-main")?.scrollTop ?? -1,
),
)
.toBe(0);
await expect(page.locator(".ad-root")).toBeInViewport();
await expect(page.locator(".vgops-root")).toBeInViewport();
await expect(page.getByRole("heading", { name: "역할, 그룹, 접근 범위" })).toBeVisible();
});
@ -848,10 +1060,12 @@ test.describe("admin route guards", () => {
await page.goto("/admin");
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
const scrolled = await page.evaluate(() => {
document.documentElement.style.minHeight = "2200px";
document.body.style.minHeight = "2200px";
window.scrollTo(0, 900);
return window.scrollY;
const main = document.querySelector<HTMLElement>(".vg-main");
const root = document.querySelector<HTMLElement>(".vgops-root");
if (!main || !root) throw new Error("admin scroll container missing");
root.style.minHeight = "2200px";
main.scrollTop = 900;
return main.scrollTop;
});
expect(scrolled).toBeGreaterThan(0);
@ -860,12 +1074,44 @@ test.describe("admin route guards", () => {
});
await expect
.poll(() => page.evaluate(() => window.scrollY))
.poll(() =>
page.evaluate(
() => document.querySelector<HTMLElement>(".vg-main")?.scrollTop ?? -1,
),
)
.toBe(0);
await expect(page.locator(".ad-root")).toBeInViewport();
await expect(page.locator(".vgops-root")).toBeInViewport();
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
});
test("keeps the admin console visible under EasyList cosmetic filters", async ({ page }) => {
await mockAdminSession(page, {
user_id: "cosmetic-filter-admin",
email: "cosmetic-filter-admin@twentyoz.kr",
display_name: "Cosmetic Filter Admin",
role: "admin",
admin_access: true,
super_admin: true,
onboarding_completed_at: 1_782_900_000,
});
await page.goto("/admin");
await page.addStyleTag({
// EasyList general cosmetic rules contain both selectors. They used to
// hide the whole operations console while every admin API still returned 200.
content: ".ad-root,.ad-section{display:none!important}",
});
await expect(page.getByRole("heading", { name: "현재 서비스 상태" })).toBeVisible();
await expect(page.locator("[data-vignette-admin-root]")).toBeVisible();
await expect(page.locator('[class^="ad-"],[class*=" ad-"]')).toHaveCount(0);
// 첫 watchdog 판정(3.5초)이 실제 픽셀 가시성을 확인한 뒤에도 진단 화면이
// 뜨지 않아야 한다. DOM 존재 여부만 확인하면 이번 장애를 재현하지 못한다.
await page.waitForTimeout(4_000);
await expect(page.locator("#vignette-boot-diagnostic")).toHaveCount(0);
});
test("shows admin data diagnostics instead of a blank main pane", async ({ page }) => {
await mockAdminSession(
page,
@ -902,9 +1148,9 @@ test.describe("admin route guards", () => {
await page.goto("/admin/users");
await expect(page.getByRole("heading", { name: "가입 승인과 권한 관리" })).toBeVisible();
await expect(page.locator(".ad-diagnostic")).toContainText("관리자 데이터 진단");
await expect(page.locator(".ad-diagnostic")).toContainText("cohort_ids");
await expect(page.locator(".ad-diagnostic")).toContainText("active_sessions");
await expect(page.locator(".vgops-diagnostic")).toContainText("관리자 데이터 진단");
await expect(page.locator(".vgops-diagnostic")).toContainText("cohort_ids");
await expect(page.locator(".vgops-diagnostic")).toContainText("active_sessions");
await expect(page.getByText("Bad User")).toBeVisible();
});
@ -933,8 +1179,8 @@ test.describe("admin route guards", () => {
await page.goto("/admin/users");
await expect(page.getByRole("heading", { name: "가입 승인과 권한 관리" })).toBeVisible();
await expect(page.locator(".ad-diagnostic")).toContainText("관리자 데이터 진단");
await expect(page.locator(".ad-diagnostic")).toContainText("admin.tickets.summary");
await expect(page.locator(".vgops-diagnostic")).toContainText("관리자 데이터 진단");
await expect(page.locator(".vgops-diagnostic")).toContainText("admin.tickets.summary");
await expect(page.locator("body")).not.toHaveText(/^$/);
});
});
@ -950,7 +1196,7 @@ test.describe("admin route", () => {
await withGlobalEngineConfigLock("admin-health-dashboard", async () => {
const { health, usage, users, uptime, tickets } = await openAdminAndReadHealth(page);
const serviceCards = page.locator(".ad-service:not(.ad-service--skeleton)");
const serviceCards = page.locator(".vgops-service:not(.vgops-service--skeleton)");
const counts = {
ok: health.services.filter((service) => service.status === "ok").length,
degraded: health.services.filter((service) => service.status === "degraded").length,
@ -964,17 +1210,17 @@ test.describe("admin route", () => {
const onlineUsers = users.users.filter((user) => isOnline(user.last_seen_at)).length;
await expect(page).toHaveURL(/\/admin$/);
await expect(page.locator(".ad-status")).toContainText(environmentLabel(health.environment));
await expect(page.locator(".ad-status")).toContainText(engineModeLabel(health.engine_mode));
await expect(page.locator(".ad-kpi b")).toHaveText([
await expect(page.locator(".vgops-status")).toContainText(environmentLabel(health.environment));
await expect(page.locator(".vgops-status")).toContainText(engineModeLabel(health.engine_mode));
await expect(page.locator(".vgops-kpi b")).toHaveText([
countLabel(activeSessions),
countLabel(onlineUsers),
countLabel(users.users.length),
counts.total ? `${counts.ok}/${counts.total}` : "-",
]);
await expect(page.getByRole("heading", { name: "AI 비용" })).toBeVisible();
await expect(page.locator(".ad-cost")).toContainText(costLabel(usage.cost_usd));
await expect(page.locator(".ad-cost")).toContainText(
await expect(page.locator(".vgops-cost")).toContainText(costLabel(usage.cost_usd));
await expect(page.locator(".vgops-cost")).toContainText(
usage.budget.status === "disabled"
? "예산 경고 비활성"
: usage.budget.status === "exceeded"
@ -983,17 +1229,17 @@ test.describe("admin route", () => {
? "예산 주의"
: "예산 정상",
);
await expect(page.locator(".ad-panel").filter({ hasText: "AI 비용" })).toContainText(
await expect(page.locator(".vgops-panel").filter({ hasText: "AI 비용" })).toContainText(
"평가 캐시 hit-rate",
);
const cache = evaluatorCache(usage);
await expect(page.locator(".ad-panel").filter({ hasText: "AI 비용" })).toContainText(
await expect(page.locator(".vgops-panel").filter({ hasText: "AI 비용" })).toContainText(
cache.enabled
? `${rateLabel(cache.hit_rate)} hit`
: "캐시 비활성",
);
if (usageDailyCost(usage).length > 0) {
await expect(page.locator(".ad-panel").filter({ hasText: "AI 비용" })).toContainText(
await expect(page.locator(".vgops-panel").filter({ hasText: "AI 비용" })).toContainText(
"일별 비용 추이",
);
}
@ -1003,7 +1249,7 @@ test.describe("admin route", () => {
expect(uptime.ok_ratio).toBeLessThanOrEqual(1);
await expect(page.getByRole("heading", { name: "운영 티켓" })).toBeVisible();
expect(tickets.summary.open_count).toBeGreaterThanOrEqual(0);
await expect(page.locator(".ad-panel").filter({ hasText: "운영 티켓" })).toContainText(
await expect(page.locator(".vgops-panel").filter({ hasText: "운영 티켓" })).toContainText(
/(\d+건 미해결|미해결 티켓이 없습니다)/,
);
await expect(serviceCards).toHaveCount(health.services.length);
@ -1128,10 +1374,10 @@ test.describe("admin route", () => {
const approvalTab = page.getByRole("tab", { name: /가입 승인/ });
await approvalTab.click();
await expect(approvalTab).toHaveAttribute("aria-selected", "true");
const approvalCard = page.locator(".ad-approval").filter({ hasText: email });
const approvalCard = page.locator(".vgops-approval").filter({ hasText: email });
await expect(approvalCard).toBeVisible();
await expect(approvalCard).toContainText("승인 대기");
await expectVisibleButtonsFit(page, ".ad-approval__actions .vg-btn", "admin approval buttons");
await expectVisibleButtonsFit(page, ".vgops-approval__actions .vg-btn", "admin approval buttons");
const approvePromise = page.waitForResponse(isAdminUserPatch(created.user_id));
await approvalCard.getByRole("button", { name: "승인" }).click();
@ -1147,11 +1393,11 @@ test.describe("admin route", () => {
await page.getByRole("tab", { name: "사용자 목록" }).click();
await page.getByLabel("사용자 검색").fill(email);
const card = page.locator(".ad-user-table tbody tr").filter({ hasText: email });
const card = page.locator(".vgops-user-table tbody tr").filter({ hasText: email });
await expect(card).toBeVisible();
await expect(card.getByLabel(`${email} 표시 이름`)).toHaveValue(displayName);
await expect(card).toContainText("승인됨");
await expectVisibleButtonsFit(page, ".ad-user__actions .vg-btn", "admin user action buttons");
await expectVisibleButtonsFit(page, ".vgops-user__actions .vg-btn", "admin user action buttons");
const nextName = `교수자 ${testInfo.project.name}`;
const nameInput = card.getByLabel(`${email} 표시 이름`);
@ -1220,7 +1466,7 @@ test.describe("admin route", () => {
expect(tickets.durable).toBe(true);
expect(tickets.tickets.some((ticket) => ticket.ticket_id === created.ticket_id)).toBeTruthy();
const card = page.locator(".ad-ticket").filter({ hasText: subject });
const card = page.locator(".vgops-ticket").filter({ hasText: subject });
await expect(card).toBeVisible();
await expect(card).toContainText("높음");
await expect(card).toContainText("미해결");
@ -1277,7 +1523,7 @@ test.describe("admin route", () => {
expect(createdTickets.some((ticket) => (ticket.duplicate_count ?? 0) > 0)).toBeTruthy();
const linkableCard = page
.locator(".ad-ticket")
.locator(".vgops-ticket")
.filter({ hasText: subject })
.filter({ has: page.getByRole("button", { name: "연결" }) });
await expect(linkableCard).toBeVisible();
@ -1293,7 +1539,7 @@ test.describe("admin route", () => {
const updated = (await patchResponse.json()) as AdminSupportTicket;
expect(updated.parent_ticket_id).toBeTruthy();
await expect(page.locator(".ad-ticket").filter({ hasText: subject }).filter({ hasText: "중복 연결" })).toBeVisible();
await expect(page.locator(".vgops-ticket").filter({ hasText: subject }).filter({ hasText: "중복 연결" })).toBeVisible();
await expectNoHorizontalOverflow(page);
} finally {
for (const ticketId of createdTicketIds) {
@ -1326,7 +1572,7 @@ test.describe("admin route", () => {
await page.goto("/admin");
await expect(page).toHaveURL(/\/learn$/);
await expect(page.locator(".ad-root")).toHaveCount(0);
await expect(page.locator(".vgops-root")).toHaveCount(0);
});
test("keeps admin controls usable at a mobile viewport", async ({ page }) => {
@ -1337,7 +1583,7 @@ test.describe("admin route", () => {
const users = await openAdminAndReadUsers(page);
await page.getByRole("tab", { name: "사용자 등록" }).click();
const layout = await page.evaluate(() => {
const form = document.querySelector<HTMLElement>(".ad-user-create");
const form = document.querySelector<HTMLElement>(".vgops-user-create");
if (!form) throw new Error("admin user create form was not rendered");
return {
formColumns: window.getComputedStyle(form).gridTemplateColumns.split(" ").length,
@ -1349,7 +1595,7 @@ test.describe("admin route", () => {
expect(layout.formColumns).toBe(1);
if (users.users.length > 0) {
await page.getByRole("tab", { name: "사용자 목록" }).click();
const scrollRegion = page.locator(".ad-user-table-scroll");
const scrollRegion = page.locator(".vgops-user-table-scroll");
const scrollContract = await scrollRegion.evaluate((element) => {
const before = element.scrollLeft;
element.scrollLeft = element.scrollWidth;
@ -1363,7 +1609,7 @@ test.describe("admin route", () => {
expect(scrollContract.scrollWidth).toBeGreaterThan(scrollContract.clientWidth);
expect(scrollContract.after).toBeGreaterThan(scrollContract.before);
await expect(page.getByRole("columnheader", { name: /작업/ })).toBeVisible();
await expectVisibleButtonsFit(page, ".ad-user__actions .vg-btn", "mobile admin user actions");
await expectVisibleButtonsFit(page, ".vgops-user__actions .vg-btn", "mobile admin user actions");
}
});