feat(ai-character): 커뮤니티 생성 미디어 흐름 개선
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import type { Locator, Page, TestInfo } from "@playwright/test";
|
||||
|
||||
// allow: SIZE_OK - ordered Community browser states intentionally share one E2E narrative.
|
||||
|
||||
const interactiveActionRoles = ["button", "link", "menuitem"] as const;
|
||||
const tinyGif = Buffer.from("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==", "base64");
|
||||
const tinyPng = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFElEQVQYV2NkYGD4z0AEYBxVSF+FAAfiAQELhYkNAAAAAElFTkSuQmCC", "base64");
|
||||
|
||||
async function captureScreenshot(page: Page, testInfo: TestInfo, name: string): Promise<void> {
|
||||
const path = testInfo.outputPath(name);
|
||||
await page.screenshot({ fullPage: true, path });
|
||||
await testInfo.attach(name, { contentType: "image/png", path });
|
||||
}
|
||||
|
||||
async function expectNoHorizontalOverflow(page: Page): Promise<void> {
|
||||
const hasHorizontalOverflow = await page.evaluate(
|
||||
@@ -12,10 +22,17 @@ async function expectNoHorizontalOverflow(page: Page): Promise<void> {
|
||||
expect(hasHorizontalOverflow).toBe(false);
|
||||
}
|
||||
|
||||
async function zoomTo200Percent(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.style.zoom = "2";
|
||||
});
|
||||
async function expectNoClippedKoreanText(root: Locator): Promise<void> {
|
||||
const clippedText = await root.evaluate((element) => Array.from(element.querySelectorAll<HTMLElement>("*"))
|
||||
.filter((candidate) => candidate.childElementCount === 0 && /[가-힣]/.test(candidate.textContent ?? ""))
|
||||
.filter((candidate) => {
|
||||
const style = getComputedStyle(candidate);
|
||||
return style.display !== "none" && style.visibility !== "hidden" && candidate.clientWidth > 0 && candidate.clientHeight > 0;
|
||||
})
|
||||
.filter((candidate) => candidate.scrollWidth > candidate.clientWidth + 1 || candidate.scrollHeight > candidate.clientHeight + 1)
|
||||
.map((candidate) => candidate.textContent?.trim()));
|
||||
|
||||
expect(clippedText).toEqual([]);
|
||||
}
|
||||
|
||||
async function expectNoCriticalOrSeriousAxeViolations(page: Page): Promise<void> {
|
||||
@@ -119,9 +136,9 @@ test("mobile Community route stays read-only while list sheet and audio remain a
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test("mobile Community create route shows guidance instead of the create form", async ({ page }) => {
|
||||
test("mobile Community create route shows guidance instead of the create form", async ({ page }, testInfo) => {
|
||||
// Given
|
||||
await page.setViewportSize({ width: 320, height: 640 });
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await loginThroughMockMode(page);
|
||||
|
||||
// When
|
||||
@@ -132,10 +149,13 @@ test("mobile Community create route shows guidance instead of the create form",
|
||||
await expect(page.getByRole("form", { name: "커뮤니티 게시글 생성 입력 화면" })).toBeHidden();
|
||||
await expectActionAbsentAcrossInteractiveRoles(page, "생성");
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await expectNoClippedKoreanText(page.locator("main"));
|
||||
await expectNoCriticalOrSeriousAxeViolations(page);
|
||||
await captureScreenshot(page, testInfo, "community-create-375-guidance.png");
|
||||
});
|
||||
|
||||
for (const width of [768, 1280] as const) {
|
||||
test(`tablet and desktop Community management remains available at ${width}px`, async ({ page }) => {
|
||||
test(`tablet and desktop Community management remains available at ${width}px`, async ({ page }, testInfo) => {
|
||||
// Given
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await loginThroughMockMode(page);
|
||||
@@ -156,8 +176,119 @@ for (const width of [768, 1280] as const) {
|
||||
await dialog.getByRole("button", { name: "닫기" }).click();
|
||||
|
||||
await page.getByRole("link", { name: "커뮤니티 게시글 생성" }).click();
|
||||
await expect(page.getByRole("form", { name: "커뮤니티 게시글 생성 입력 화면" })).toBeVisible();
|
||||
const form = page.getByRole("form", { name: "커뮤니티 게시글 생성 입력 화면" });
|
||||
const imageInput = form.locator("input[type='file']").first();
|
||||
const audioInput = form.locator("input[type='file']").nth(1);
|
||||
const imageSelectButton = form.getByRole("button", { name: "게시글 이미지 파일 선택" });
|
||||
const contentInput = form.getByRole("textbox", { name: "내용" });
|
||||
const priceInput = form.getByRole("spinbutton", { name: "가격" });
|
||||
|
||||
await expect(form).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "생성" })).toBeVisible();
|
||||
await expect(audioInput).toBeHidden();
|
||||
await expect(priceInput).toHaveValue("0");
|
||||
expect(await form.evaluate((element) => {
|
||||
const image = element.querySelector("input[type='file']");
|
||||
const content = element.querySelector("textarea");
|
||||
return image !== null && content !== null && Boolean(image.compareDocumentPosition(content) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
})).toBe(true);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await expectNoClippedKoreanText(form);
|
||||
await expectNoCriticalOrSeriousAxeViolations(page);
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-initial.png`);
|
||||
|
||||
await pressTabUntilFocused(page, imageSelectButton);
|
||||
await expect(imageSelectButton).toBeFocused();
|
||||
expect(await imageSelectButton.evaluate((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
return style.outlineStyle !== "none" || style.boxShadow !== "none";
|
||||
})).toBe(true);
|
||||
|
||||
const imagePreview = form.getByRole("img", { name: "게시글 이미지 업로드 미리보기" });
|
||||
const cropDialog = page.getByRole("dialog", { name: "이미지 crop" });
|
||||
|
||||
await imageInput.setInputFiles({ buffer: tinyPng, mimeType: "image/png", name: "community-crop.png" });
|
||||
await expect(cropDialog).toBeVisible();
|
||||
await expect(imagePreview).toBeHidden();
|
||||
await expect(audioInput).toBeHidden();
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-crop.png`);
|
||||
|
||||
await cropDialog.getByRole("button", { name: "적용" }).click();
|
||||
await expect(cropDialog).toBeHidden();
|
||||
await expect(imagePreview).toBeVisible();
|
||||
expect(await imagePreview.evaluate((image) => image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0)).toBe(true);
|
||||
await expect(audioInput).toBeVisible();
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-applied.png`);
|
||||
|
||||
await audioInput.setInputFiles({ buffer: Buffer.from("ID3"), mimeType: "audio/mpeg", name: "community-audio.mp3" });
|
||||
await imageInput.setInputFiles({ buffer: tinyPng, mimeType: "image/png", name: "community-replacement.png" });
|
||||
await expect(cropDialog).toBeVisible();
|
||||
await expect(imagePreview).toBeHidden();
|
||||
await expect(audioInput).toBeHidden();
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-replacement.png`);
|
||||
|
||||
await cropDialog.getByRole("button", { name: "취소" }).click();
|
||||
await expect(cropDialog).toBeHidden();
|
||||
await expect(imagePreview).toBeHidden();
|
||||
await expect(audioInput).toBeHidden();
|
||||
|
||||
await imageInput.setInputFiles({ buffer: Buffer.from("not-an-image"), mimeType: "image/png", name: "community-invalid.png" });
|
||||
const imageErrorAlert = form.getByRole("alert");
|
||||
await expect(cropDialog).toBeHidden();
|
||||
await expect(imagePreview).toBeHidden();
|
||||
await expect(audioInput).toBeHidden();
|
||||
await expect(imageErrorAlert).toContainText("이미지 미리보기를 불러오지 못했습니다.");
|
||||
await expect(imageErrorAlert).toBeVisible();
|
||||
await imageErrorAlert.scrollIntoViewIfNeeded();
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-error.png`);
|
||||
|
||||
await imageInput.setInputFiles({ buffer: tinyGif, mimeType: "image/gif", name: "community-final.gif" });
|
||||
await expect(imagePreview).toBeVisible();
|
||||
expect(await imagePreview.evaluate((image) => image instanceof HTMLImageElement && image.complete && image.naturalWidth > 0)).toBe(true);
|
||||
await expect(audioInput).toBeVisible();
|
||||
expect(await form.evaluate((element) => {
|
||||
const [image, audio] = element.querySelectorAll("input[type='file']");
|
||||
const content = element.querySelector("textarea");
|
||||
return image !== undefined && audio !== undefined && content !== null
|
||||
&& Boolean(image.compareDocumentPosition(audio) & Node.DOCUMENT_POSITION_FOLLOWING)
|
||||
&& Boolean(audio.compareDocumentPosition(content) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
})).toBe(true);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await expectNoClippedKoreanText(form);
|
||||
await expectNoCriticalOrSeriousAxeViolations(page);
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-gif-ready.png`);
|
||||
|
||||
await audioInput.setInputFiles({ buffer: Buffer.from("ID3"), mimeType: "audio/mpeg", name: "community-audio.mp3" });
|
||||
await expect(audioInput).not.toHaveValue("");
|
||||
await form.getByRole("button", { name: "게시글 이미지 선택 취소" }).click();
|
||||
await expect(form.locator("img")).toBeHidden();
|
||||
await expect(audioInput).toBeHidden();
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await expectNoClippedKoreanText(form);
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-image-removed.png`);
|
||||
|
||||
await imageInput.setInputFiles({ buffer: tinyGif, mimeType: "image/gif", name: "community-final.gif" });
|
||||
await expect(audioInput).toBeVisible();
|
||||
await expect(audioInput).toHaveValue("");
|
||||
await contentInput.fill("가격 검증을 위한 커뮤니티 게시글입니다.");
|
||||
await priceInput.fill("");
|
||||
const createRequests: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (request.method() === "POST" && request.url().includes("/community-posts")) {
|
||||
createRequests.push(request.url());
|
||||
}
|
||||
});
|
||||
await page.getByRole("button", { name: "생성" }).click();
|
||||
await expect(page.getByRole("alert")).toContainText("가격은 0 이상 99,999 이하 정수 캔으로 입력하세요.");
|
||||
await expect(priceInput).toBeFocused();
|
||||
await expect(priceInput).toHaveValue("");
|
||||
await expect(page).toHaveURL(/\/ai-characters\/101\/community-posts\/new$/);
|
||||
expect(createRequests).toEqual([]);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
await expectNoClippedKoreanText(form);
|
||||
await expectNoCriticalOrSeriousAxeViolations(page);
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await captureScreenshot(page, testInfo, `community-create-${width}-blank-price-error.png`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,20 +322,19 @@ test("Community sheet traps focus closes on Escape and returns focus", async ({
|
||||
await expect(opener).toBeFocused();
|
||||
});
|
||||
|
||||
test("Community list and create routes keep zoom overflow and axe coverage across capability widths", async ({ page }) => {
|
||||
test("Community list and create routes keep overflow and axe coverage across capability widths", async ({ page }) => {
|
||||
// Given
|
||||
await loginThroughMockMode(page);
|
||||
|
||||
for (const width of [320, 768, 1280] as const) {
|
||||
for (const width of [320, 375, 768, 1280] as const) {
|
||||
for (const route of ["/ai-characters/101/community-posts", "/ai-characters/101/community-posts/new"] as const) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto(route);
|
||||
|
||||
// When
|
||||
await zoomTo200Percent(page);
|
||||
|
||||
// Then
|
||||
await expectNoHorizontalOverflow(page);
|
||||
if (width !== 320) {
|
||||
await expectNoClippedKoreanText(page.locator("main"));
|
||||
}
|
||||
await expectNoCriticalOrSeriousAxeViolations(page);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user