vignette/apps/web/e2e/image-resilience.spec.ts

157 lines
5.4 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
const PNG_1X1_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lTQvYwAAAABJRU5ErkJggg==";
function jsonRoute(body: unknown, status = 200) {
return { status, contentType: "application/json", body: JSON.stringify(body) };
}
async function installOnboardingFixtures(page: Page, avatarUrl: string) {
await page.route("**/api/**", (route) =>
route.fulfill(jsonRoute({ detail: "not part of this focused fixture" }, 404)),
);
await page.route("**/api/auth/me", (route) =>
route.fulfill(
jsonRoute({
user_id: "e2e-image-resilience",
email: "avatar.fixture@hs.ac.kr",
role: "learner",
display_name: "온보딩 사용자",
admin_access: false,
super_admin: false,
account_status: "approved",
approval_required: false,
cohort_ids: [],
consent_at: null,
onboarding_completed_at: null,
nickname: "온보딩 사용자",
self_introduction: "",
avatar_url: avatarUrl,
}),
),
);
await page.route("**/api/users/me", (route) =>
route.fulfill(
jsonRoute({
user_id: "e2e-image-resilience",
email: "avatar.fixture@hs.ac.kr",
role: "learner",
display_name: "온보딩 사용자",
legal_name: "",
affiliation: "한신대학교",
department: "",
grade_level: "",
phone: "",
contact_address: "",
nickname: "온보딩 사용자",
self_introduction: "",
avatar_url: avatarUrl,
cohort_ids: [],
}),
),
);
await page.route("**/api/users/legal-docs", (route) =>
route.fulfill(
jsonRoute({
terms: {
kind: "terms",
status: "draft",
title: "서비스 이용약관",
version: "image-resilience-fixture",
body: "테스트 약관",
},
privacy: {
kind: "privacy",
status: "draft",
title: "개인정보 처리방침",
version: "image-resilience-fixture",
body: "테스트 개인정보 처리방침",
},
source_note: "이미지 회복성 전용 fixture",
}),
),
);
}
test.describe("사용자 이미지 회복성", () => {
test("대문자 외부 HTTPS URL도 안전하게 정규화해 실제 이미지로 전환한다", async ({
page,
}) => {
const externalAvatar = "HTTPS://images.example.test/profile/avatar.png";
await installOnboardingFixtures(page, externalAvatar);
await page.route("https://images.example.test/profile/avatar.png", (route) =>
route.fulfill({
status: 200,
contentType: "image/png",
body: Buffer.from(PNG_1X1_BASE64, "base64"),
}),
);
await page.goto("/onboarding");
const preview = page.locator(".ob-avatar__preview");
const image = preview.locator("img");
await expect(image).toBeVisible();
await expect(image).toHaveAttribute(
"src",
"https://images.example.test/profile/avatar.png",
);
await expect(preview.locator("[data-image-fallback]")).toHaveCount(0);
});
test("온보딩 아바타 파일이 404여도 깨진 아이콘 대신 닉네임 이니셜을 표시한다", async ({
page,
}) => {
const staleAvatar = "/uploads/profile-avatars/stale-onboarding-avatar.jpg";
await installOnboardingFixtures(page, staleAvatar);
await page.route("**/api/uploads/profile-avatars/stale-onboarding-avatar.jpg", (route) =>
route.fulfill(jsonRoute({ detail: "Not Found" }, 404)),
);
await page.goto("/onboarding");
await expect(page.getByRole("heading", { name: "가입 정보를 입력합니다." })).toBeVisible();
const preview = page.locator(".ob-avatar__preview");
await expect(preview.locator("[data-image-fallback='failed']")).toHaveText("온");
await expect(preview.locator("img")).toHaveCount(0);
});
test("지연된 이미지 응답 중에도 브라우저의 깨진 이미지 박스를 노출하지 않는다", async ({
page,
}) => {
const delayedAvatar = "/uploads/profile-avatars/delayed-onboarding-avatar.jpg";
let releaseImage: (() => void) | undefined;
const imageGate = new Promise<void>((resolve) => {
releaseImage = resolve;
});
await installOnboardingFixtures(page, delayedAvatar);
await page.route("**/api/uploads/profile-avatars/delayed-onboarding-avatar.jpg", async (route) => {
await imageGate;
await route.fulfill({
status: 200,
contentType: "image/png",
body: Buffer.from(PNG_1X1_BASE64, "base64"),
});
});
await page.goto("/onboarding", { waitUntil: "domcontentloaded" });
const preview = page.locator(".ob-avatar__preview");
await expect(preview.locator("[data-image-fallback='loading']")).toHaveText("온");
const pendingImage = preview.locator("img");
await expect(pendingImage).toHaveAttribute("data-image-state", "loading");
await expect(pendingImage).toBeHidden();
await expect(pendingImage).toHaveCSS("display", "none");
expect(await pendingImage.evaluate((image) => image.getBoundingClientRect().toJSON())).toMatchObject({
width: 0,
height: 0,
});
releaseImage?.();
await expect(pendingImage).toBeVisible();
await expect(pendingImage).toHaveAttribute("data-image-state", "ready");
await expect(preview.locator("[data-image-fallback]")).toHaveCount(0);
});
});