157 lines
5.4 KiB
TypeScript
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);
|
|
});
|
|
});
|