관리자 설정 로딩과 부트 진단 복구

This commit is contained in:
Yun Chan 2026-09-22 23:06:54 +09:00
parent d58eb67764
commit fb640a75ac
7 changed files with 767 additions and 100 deletions

View file

@ -232,9 +232,15 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
await page.goto("/learn");
await expect(page.getByText(BOOT_SCREEN_TEXT)).toBeVisible();
const bootScreen = page.getByText(BOOT_SCREEN_TEXT);
await expect(bootScreen).toBeVisible();
await expect(bootScreen).toHaveAttribute("data-vignette-boot-loading", "true");
// 부트스트랩이 끝나지 않는 동안은 로그인으로 이동하지 않는다
await expect(page).toHaveURL(/\/learn$/);
// 3.5초와 8초 watchdog은 화면에 실제로 보이는 bounded BootScreen을
// 빈 React root로 오인하지 않는다.
await page.waitForTimeout(8_600);
await expect(page.locator("#vignette-boot-diagnostic")).toHaveCount(0);
releaseMe();
@ -242,6 +248,154 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
});
test("removes a watchdog panel when the delayed boot screen becomes visible", async ({ page }) => {
test.setTimeout(15_000);
let releaseMain!: () => void;
const mainGate = new Promise<void>((resolve) => {
releaseMain = resolve;
});
let releaseMe!: () => void;
const meGate = new Promise<void>((resolve) => {
releaseMe = resolve;
});
await page.route("**/src/main.tsx*", async (route) => {
await mainGate;
await route.continue();
});
await page.route("**/api/auth/me", async (route) => {
await meGate;
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ detail: "not authenticated" }),
});
});
await page.goto("/learn", { waitUntil: "commit" });
await expect(page.locator("#root")).toBeEmpty();
await expect(page.locator("#vignette-boot-diagnostic")).toBeVisible({ timeout: 4_500 });
releaseMain();
await expect(page.getByText(BOOT_SCREEN_TEXT)).toHaveAttribute(
"data-vignette-boot-loading",
"true",
);
await expect(page.locator("#vignette-boot-diagnostic")).toHaveCount(0, { timeout: 1_500 });
releaseMe();
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
});
test("reports a boot screen that remains visible through the 20-second watchdog", async ({ page }) => {
test.setTimeout(35_000);
let releaseChunk!: () => void;
const gate = new Promise<void>((resolve) => {
releaseChunk = resolve;
});
await page.route("**/src/pages/Login.tsx*", async (route) => {
await gate;
await route.continue();
});
await page.route("**/api/auth/me", async (route) => {
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ detail: "not authenticated" }),
});
});
await page.route("**/api/auth/config", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
google_oauth_configured: true,
saml_configured: false,
providers: [
{
provider: "google",
configured: true,
enabled: true,
login_path: "/auth/login?provider=google",
},
],
allowed_email_domains: [],
redirect_uri: "http://127.0.0.1:8000/auth/callback",
dev_login_enabled: false,
}),
});
});
const meResolved = page.waitForResponse((response) => response.url().includes("/api/auth/me"));
await page.goto("/login", { waitUntil: "domcontentloaded" });
await meResolved;
await expect(page.getByText(BOOT_SCREEN_TEXT).filter({ visible: true })).toHaveAttribute(
"data-vignette-boot-loading",
"true",
);
await page.waitForTimeout(8_600);
await expect(page.locator("#vignette-boot-diagnostic")).toHaveCount(0);
await expect(page.locator("#vignette-boot-diagnostic")).toBeVisible({ timeout: 14_000 });
await expect(page.locator("#vignette-boot-diagnostic")).toHaveAttribute(
"data-vignette-boot-reason",
"watchdog",
);
releaseChunk();
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
});
test("keeps manual and runtime diagnostics when a delayed boot screen appears after watchdog", async ({ page }) => {
test.setTimeout(15_000);
let releaseMain!: () => void;
const mainGate = new Promise<void>((resolve) => {
releaseMain = resolve;
});
let releaseMe!: () => void;
const meGate = new Promise<void>((resolve) => {
releaseMe = resolve;
});
await page.route("**/src/main.tsx*", async (route) => {
await mainGate;
await route.continue();
});
await page.route("**/api/auth/me", async (route) => {
await meGate;
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ detail: "not authenticated" }),
});
});
await page.goto("/learn", { waitUntil: "commit" });
const diagnostic = page.locator("#vignette-boot-diagnostic");
await expect(diagnostic).toBeVisible({ timeout: 4_500 });
await expect(diagnostic).toHaveAttribute("data-vignette-boot-reason", "watchdog");
await page.evaluate(() => {
(window as Window & { __VIGNETTE_BOOT_DIAG__?: { check(): void } }).__VIGNETTE_BOOT_DIAG__?.check();
});
await expect(diagnostic).toHaveAttribute("data-vignette-boot-reason", "manual");
await page.evaluate(() => {
window.dispatchEvent(new ErrorEvent("error", { message: "boot diagnostic runtime fixture" }));
});
await expect(diagnostic).toHaveAttribute("data-vignette-boot-reason", "runtime-error");
releaseMain();
await expect(page.getByText(BOOT_SCREEN_TEXT)).toHaveAttribute(
"data-vignette-boot-loading",
"true",
);
await page.waitForTimeout(250);
await expect(diagnostic).toBeVisible();
await expect(diagnostic).toHaveAttribute("data-vignette-boot-reason", "runtime-error");
await expect(diagnostic.locator("dd").first()).toHaveText("runtime-error");
releaseMe();
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
});
// checklist: shell-suspense-fallback
// 모든 페이지가 lazy import이므로 라우트 청크가 로드되는 동안
// Suspense fallback(BootScreen)이 표시된다. Vite dev 모듈 URL을 지연시켜 재현.
@ -254,7 +408,35 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
await gate;
await route.continue();
});
await page.route("**/api/auth/me", async (route) => {
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ detail: "not authenticated" }),
});
});
await page.route("**/api/auth/config", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
google_oauth_configured: true,
saml_configured: false,
providers: [
{
provider: "google",
configured: true,
enabled: true,
login_path: "/auth/login?provider=google",
},
],
allowed_email_domains: [],
redirect_uri: "http://127.0.0.1:8000/auth/callback",
dev_login_enabled: false,
}),
});
});
const meResolved = page.waitForResponse(
(res) => res.url().includes("/api/auth/me"),
{ timeout: 15_000 },
@ -285,7 +467,35 @@ test.describe("full sweep — shared shell, GNB, and routing guards", () => {
page.on("request", (request) => {
if (request.isNavigationRequest()) documentRequests += 1;
});
await page.route("**/api/auth/me", async (route) => {
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ detail: "not authenticated" }),
});
});
await page.route("**/api/auth/config", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
google_oauth_configured: true,
saml_configured: false,
providers: [
{
provider: "google",
configured: true,
enabled: true,
login_path: "/auth/login?provider=google",
},
],
allowed_email_domains: [],
redirect_uri: "http://127.0.0.1:8000/auth/callback",
dev_login_enabled: false,
}),
});
});
await page.goto("/login");
await expect(page.getByRole("button", { name: /Google 계정으로 로그인/ })).toBeVisible();
expect(documentRequests).toBe(1);