케이스 이어하기 UI와 공통 탭·이미지 복구를 반영
This commit is contained in:
parent
72353ecd82
commit
f1b80676c1
38 changed files with 3581 additions and 455 deletions
157
apps/web/e2e/image-resilience.spec.ts
Normal file
157
apps/web/e2e/image-resilience.spec.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue