feat(ai-character): 오디오 콘텐츠 폼 입력 UX 정렬

This commit is contained in:
Yu Sung
2026-08-04 12:20:04 +09:00
parent a213479e8e
commit b62c416fa1
13 changed files with 608 additions and 173 deletions

View File

@@ -106,7 +106,7 @@ test("desktop and tablet expose audio mutation and upload actions", async ({ pag
}
});
test("desktop create form exposes create settings and rejects raw invalid price", async ({ page }) => {
test("desktop create form exposes P4-R10 native controls and conditional states without overflow", async ({ page }) => {
// Given
let createRequests = 0;
page.on("request", (request) => {
@@ -121,26 +121,150 @@ test("desktop create form exposes create settings and rejects raw invalid price"
// When
await page.goto("/ai-characters/101/audio-contents/new");
// Then
// Then: free state
await expect(page.getByRole("form", { name: "오디오 콘텐츠 생성 입력 화면" })).toBeVisible();
await expect(page.getByLabel("구매 옵션")).toBeVisible();
await expect(page.getByLabel("기간제")).toBeVisible();
await expect(page.getByRole("group", { name: "구매 옵션" })).toHaveCount(0);
await expect(page.getByLabel("미리듣기 생성")).toHaveCount(0);
await expect(page.getByLabel("포인트 사용")).toHaveCount(0);
await expect(page.getByLabel("미리듣기 시작")).toHaveCount(0);
await expect(page.getByLabel("미리듣기 종료")).toHaveCount(0);
await expect(page.getByLabel("예약 공개일")).toHaveCount(0);
const priceInput = page.getByLabel("가격", { exact: true });
await expect(priceInput).toHaveAttribute("type", "number");
await expect(priceInput).toHaveAttribute("min", "0");
await expect(priceInput).toHaveAttribute("step", "1");
await expect(priceInput).toHaveValue("0");
await priceInput.focus();
await page.keyboard.press("ArrowDown");
await expect(priceInput).toHaveValue("0");
await page.keyboard.press("ArrowUp");
await expect(priceInput).toHaveValue("1");
await priceInput.fill("0");
await expectNoHorizontalOverflow(page);
// When: tags commit through both keyboard separators
const tagInput = page.getByLabel("태그", { exact: true });
await tagInput.fill("상담");
await tagInput.press("Enter");
await tagInput.fill("힐링");
await tagInput.press(",");
// Then
await expect(tagInput).toHaveValue("");
await expect(page.getByRole("button", { name: "태그 상담 삭제" })).toBeVisible();
await expect(page.getByRole("button", { name: "태그 힐링 삭제" })).toBeVisible();
expect(createRequests).toBe(0);
// When
await page.getByRole("button", { name: "태그 상담 삭제" }).click();
// Then
await expect(page.getByRole("button", { name: "태그 상담 삭제" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "태그 힐링 삭제" })).toBeVisible();
// When: a positive normalized price enables paid controls
await priceInput.fill("1000");
// Then: paid state
await expect(priceInput).toHaveValue("1000");
const purchaseOptions = page.getByRole("group", { name: "구매 옵션" });
await expect(purchaseOptions).toBeVisible();
const bothRadio = page.getByRole("radio", { name: "구매/대여" });
const buyOnlyRadio = page.getByRole("radio", { name: "구매 전용" });
const rentOnlyRadio = page.getByRole("radio", { name: "대여 전용" });
await expect(bothRadio).toHaveAttribute("type", "radio");
await expect(buyOnlyRadio).toHaveAttribute("type", "radio");
await expect(rentOnlyRadio).toHaveAttribute("type", "radio");
await expect(bothRadio).toBeChecked();
await rentOnlyRadio.focus();
await page.keyboard.press("Space");
await expect(rentOnlyRadio).toBeChecked();
await expect(page.getByLabel("기간제")).toHaveCount(0);
await expect(page.getByRole("checkbox", { name: "대여 전용" })).toHaveCount(0);
await expect(page.getByLabel("언어 코드")).toHaveCount(0);
await expect(page.getByLabel("성인 콘텐츠")).toBeVisible();
await expect(page.getByLabel("미리듣기 생성")).toBeVisible();
await expect(page.getByLabel("대여 전용")).toBeVisible();
await expect(page.getByLabel("포인트 사용")).toBeVisible();
await expect(page.getByLabel("댓글 허용")).toBeVisible();
await expect(page.getByLabel("상세 정보 전체 공개")).toBeVisible();
await expect(page.getByLabel("미리듣기 시작")).toBeVisible();
await expect(page.getByLabel("미리듣기 종료")).toBeVisible();
await expect(page.getByLabel("언어 코드")).toBeVisible();
await expectNoHorizontalOverflow(page);
// When
await page.getByLabel("가격").fill("-1");
// When: preview is enabled
const previewCheckbox = page.getByLabel("미리듣기 생성");
await previewCheckbox.check();
// Then: preview duration offsets
const previewStart = page.getByLabel("미리듣기 시작");
const previewEnd = page.getByLabel("미리듣기 종료");
await expect(previewStart).toHaveAttribute("type", "text");
await expect(previewStart).toHaveAttribute("placeholder", "예: 00:00:30");
await expect(previewStart).toHaveAttribute("pattern", "[0-9]{2}:[0-9]{2}:[0-9]{2}");
await expect(previewStart).not.toHaveAttribute("step");
await expect(previewEnd).toHaveAttribute("type", "text");
await expect(previewEnd).toHaveAttribute("placeholder", "예: 01:00:05");
await expect(previewEnd).toHaveAttribute("pattern", "[0-9]{2}:[0-9]{2}:[0-9]{2}");
await expect(previewEnd).not.toHaveAttribute("step");
await expectNoHorizontalOverflow(page);
// When / Then: unchecking preview clears the visible time controls
await previewCheckbox.uncheck();
await expect(previewStart).toHaveCount(0);
await expect(previewEnd).toHaveCount(0);
// When: preview state is populated before returning to free
await previewCheckbox.check();
await page.getByLabel("포인트 사용").check();
await previewStart.fill("00:00:30");
await previewEnd.fill("01:00:05");
// Then: immediate mode has no datetime
await expect(page.getByLabel("즉시 공개")).toBeChecked();
await expect(page.getByLabel("예약 공개일")).toHaveCount(0);
// When: scheduled mode is selected
await page.getByLabel("예약 공개").check();
// Then: exactly one native scheduled datetime is rendered
const releaseDateTime = page.getByLabel("예약 공개일");
await expect(releaseDateTime).toHaveCount(1);
await expect(releaseDateTime).toHaveAttribute("type", "datetime-local");
await releaseDateTime.fill("2026-08-05T12:00");
await expectNoHorizontalOverflow(page);
// When / Then: immediate mode removes and clears the scheduled datetime
await page.getByLabel("즉시 공개").check();
await expect(releaseDateTime).toHaveCount(0);
await page.getByLabel("예약 공개").check();
await expect(page.getByLabel("예약 공개일")).toHaveValue("");
await page.getByLabel("즉시 공개").check();
// When: returning to zero resets and hides paid state
await priceInput.fill("0");
// Then
await expect(purchaseOptions).toHaveCount(0);
await expect(previewCheckbox).toHaveCount(0);
await expect(page.getByLabel("포인트 사용")).toHaveCount(0);
await expect(previewStart).toHaveCount(0);
await expect(previewEnd).toHaveCount(0);
await expectNoHorizontalOverflow(page);
// When: paid controls return
await priceInput.fill("1");
// Then: their reset defaults are visible
await expect(page.getByRole("radio", { name: "구매/대여" })).toBeChecked();
await expect(page.getByRole("radio", { name: "대여 전용" })).not.toBeChecked();
await expect(page.getByLabel("미리듣기 생성")).not.toBeChecked();
await expect(page.getByLabel("포인트 사용")).not.toBeChecked();
await expect(page.getByLabel("미리듣기 시작")).toHaveCount(0);
await expect(page.getByLabel("미리듣기 종료")).toHaveCount(0);
// When: the preserved max boundary is exceeded
await priceInput.fill("100000");
await page.getByRole("button", { name: "생성" }).click();
// Then
await expect(page.getByLabel("가격")).toHaveValue("-1");
await expect(priceInput).toHaveValue("100000");
await expect(page.getByText("가격은 0 이상 99,999 이하 정수 캔으로 입력하세요.")).toBeVisible();
expect(createRequests).toBe(0);
});