vignette/apps/web/e2e/readiness.spec.ts

101 lines
4.4 KiB
TypeScript

import { expect, test, type APIResponse, type Page } from "@playwright/test";
import { completeOnboarding, expectNoHorizontalOverflow } from "./support";
async function expectResponseOk(response: APIResponse) {
if (!response.ok()) {
expect(response.ok(), await response.text()).toBeTruthy();
}
}
async function signIn(
page: Page,
role: "learner" | "admin",
email: string,
displayName: string,
) {
const res = await page.request.post("/api/auth/dev-login", {
data: { email, role, display_name: displayName },
});
await expectResponseOk(res);
await completeOnboarding(page, {
legal_name: displayName,
affiliation: "한신대학교",
department: role === "admin" ? "운영팀" : "상담심리학과",
grade_level: role === "admin" ? "관리자" : "3학년",
phone: "010-7777-7777",
contact_address: "경기도 오산시 한신대학교",
nickname: displayName,
self_introduction: `${displayName} readiness E2E 사용자입니다.`,
});
}
test.describe("production readiness gates", () => {
test("learner-facing data comes from server persistence, not browser or seed mocks", async ({
page,
}, testInfo) => {
const email = `readiness.${testInfo.project.name}.${testInfo.workerIndex}.${Date.now()}@hs.ac.kr`;
await signIn(page, "learner", email.toLowerCase(), "Readiness Learner");
const personaResponse = await page.request.get("/api/personas");
await expectResponseOk(personaResponse);
expect(personaResponse.headers()["x-vignette-catalog-source"]).toBe("database");
const personas = (await personaResponse.json()) as Array<{
source: string;
degraded: boolean;
}>;
expect(personas.length).toBeGreaterThan(0);
expect(personas.every((persona) => persona.source === "database" && !persona.degraded)).toBe(
true,
);
const sessionsResponse = await page.request.get("/api/sessions");
await expectResponseOk(sessionsResponse);
const sessions = (await sessionsResponse.json()) as { source: string; sessions: unknown[] };
expect(sessions).toMatchObject({ source: "database", sessions: [] });
await page.goto("/learn");
await expect(page.getByText("지금까지 12회 연습했어요")).toHaveCount(0);
await expect(page.getByText("최근 8회")).toHaveCount(0);
await expect(page.getByRole("heading", { name: "오늘 이어갈 회기를 먼저 봅니다." })).toBeVisible();
const statusRegion = page.getByRole("region", { name: "학습 현황" });
await expect(statusRegion.getByRole("button", { name: /진행 회기 0회/ })).toBeVisible();
await expect(statusRegion.getByRole("button", { name: /리뷰 대기 0건/ })).toBeVisible();
// 중간 폭 이하에서는 기록 패널이 '기록 · 리뷰' 탭 안에 있다.
const dashTabs = page.locator(".lh-tabs");
if (await dashTabs.isVisible().catch(() => false)) {
await dashTabs.locator("button", { hasText: "기록 · 리뷰" }).click();
}
await expect(page.getByText("최근 기록이 없습니다.")).toBeVisible();
if (await dashTabs.isVisible().catch(() => false)) {
await dashTabs.locator("button", { hasText: "오늘의 회기" }).click();
}
await expect(page.getByRole("button", { name: "학습 대상 선택" })).toBeVisible();
await expectNoHorizontalOverflow(page);
});
test("admin-owned runtime controls are durable database state", async ({ page }) => {
await signIn(page, "admin", "readiness-admin@twentyoz.kr", "Readiness Admin");
const usersResponse = await page.request.get("/api/admin/users");
await expectResponseOk(usersResponse);
const users = (await usersResponse.json()) as { source: string; durable: boolean };
expect(users).toMatchObject({ source: "database", durable: true });
const engineResponse = await page.request.get("/api/admin/engine-config");
await expectResponseOk(engineResponse);
const engine = (await engineResponse.json()) as {
durable: boolean;
source: string;
engine_mode: string;
engine_url: string;
model: string;
reasoning_effort: string | null;
};
expect(engine).toMatchObject({ durable: true, source: "database" });
expect(["claude_cli", "claude_api", "codex_cli", "agy_cli", "openai", "solar"]).toContain(
engine.engine_mode,
);
expect(engine.engine_url).toMatch(/^https?:\/\//);
expect(engine.model.trim().length).toBeGreaterThan(0);
});
});