Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 4 additions & 42 deletions __tests__/align-ui.spec.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { screen } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
renderWithWebpageResult,
renderWithYoutubeResult,
} from "@/__tests__/helpers";
import { renderWithWebpageResult } from "@/__tests__/helpers";

describe("align-ui", () => {
beforeEach(() => {
Expand Down Expand Up @@ -69,55 +66,20 @@ describe("align-ui", () => {
});
});

// ALIGN-005: YouTube/웹페이지 결과의 요소 순서와 gap이 동일
describe("ALIGN-005: YouTube/웹페이지 결과의 요소 순서와 gap이 동일", () => {
it("YouTube 결과의 최상위 컨테이너 gap이 gap-4이다", async () => {
await renderWithYoutubeResult();

const separator = document.querySelector("[data-testid='result-container']");
expect(separator).not.toBeNull();
const className = separator?.getAttribute("class") ?? "";
expect(className).toContain("gap-4");
});

// ALIGN-005: 웹페이지 결과의 요소 순서와 gap
describe("ALIGN-005: 웹페이지 결과의 요소 순서와 gap", () => {
it("웹페이지 결과의 최상위 컨테이너 gap이 gap-4이다", async () => {
await renderWithWebpageResult({
thumbnail: "https://example.com/og.jpg",
});
await renderWithWebpageResult({});

const container = document.querySelector("[data-testid='result-container']");
expect(container).not.toBeNull();
const className = container?.getAttribute("class") ?? "";
expect(className).toContain("gap-4");
});

it("YouTube와 웹페이지 결과의 요소 순서가 동일하다 (썸네일 → 제목/출처 → 액션 → 콘텐츠)", async () => {
await renderWithYoutubeResult();

const container = document.querySelector("[data-testid='result-container']");
expect(container).not.toBeNull();

const children = Array.from(container!.children);
const firstChild = children[0];
expect(firstChild.tagName.toLowerCase()).toBe("div");
expect(firstChild.querySelector("img")).not.toBeNull();
const secondChild = children[1];
expect(secondChild.tagName.toLowerCase()).toBe("div");
});
});

// ALIGN-009: 프롬프트 영역과 본문 사이에 구분선
describe("ALIGN-009: 프롬프트 영역과 본문 사이 구분선", () => {
it("YouTube 결과에서 프롬프트 영역과 본문 사이에 Separator가 표시된다", async () => {
await renderWithYoutubeResult();

const container = document.querySelector("[data-testid='result-container']");
expect(container).not.toBeNull();

const separators = container!.querySelectorAll('[role="separator"], [data-slot="separator"]');
expect(separators.length).toBeGreaterThanOrEqual(1);
});

it("웹페이지 결과에서 프롬프트 영역과 본문 사이에 Separator가 표시된다", async () => {
await renderWithWebpageResult({});

Expand Down
62 changes: 6 additions & 56 deletions __tests__/feedme.spec.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("feedme spec tests", () => {
const user = userEvent.setup();
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => "# 웹 접근성 가이드라인 소개\n\n웹 접근성은 장애 여부와 관계없이 모든 사람이 웹 콘텐츠를 이용할 수 있도록 보장합니다.",
json: async () => ({ title: "웹 접근성 가이드라인 소개", content: "# 웹 접근성 가이드라인 소개\n\n웹 접근성은 장애 여부와 관계없이 모든 사람이 웹 콘텐츠를 이용할 수 있도록 보장합니다." }),
} as Response);

render(<ContentExtractor />);
Expand All @@ -39,39 +39,13 @@ describe("feedme spec tests", () => {
await user.click(button);

await waitFor(() => {
expect(screen.getByText(/웹 접근성 가이드라인 소개/)).toBeInTheDocument();
expect(screen.getAllByText(/웹 접근성 가이드라인 소개/).length).toBeGreaterThan(0);
});

expect(screen.getByText(/웹 접근성은 장애 여부와 관계없이/)).toBeInTheDocument();
});
});

// FEEDME-002
describe("FEEDME-002: YouTube URL 추출", () => {
it("YouTube URL 입력 후 가져오기 버튼 클릭 시 제목, 채널명, 자막이 렌더링된 마크다운으로 표시된다", async () => {
const user = userEvent.setup();
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => "---\ntitle: \"Never Gonna Give You Up\"\nauthor: \"Rick Astley\"\nsite: \"YouTube\"\n---\n\n# Never Gonna Give You Up\n\n채널: Rick Astley\n\n## 자막\n\nWe're no strangers to love",
} as Response);

render(<ContentExtractor />);

const input = screen.getByRole("textbox");
await user.type(input, "https://www.youtube.com/watch?v=dQw4w9WgXcQ");

const button = screen.getByRole("button", { name: "가져오기" });
await user.click(button);

await waitFor(() => {
expect(screen.getAllByText(/Never Gonna Give You Up/).length).toBeGreaterThan(0);
});

expect(screen.getAllByText(/Rick Astley/).length).toBeGreaterThan(0);
expect(screen.getByText(/We're no strangers to love/)).toBeInTheDocument();
});
});

// FEEDME-003
describe("FEEDME-003: 로딩 스피너", () => {
it("가져오기 버튼 클릭 직후 버튼에 스피너가 표시되고 텍스트는 사라진다", async () => {
Expand Down Expand Up @@ -104,7 +78,7 @@ describe("feedme spec tests", () => {

resolveResponse!({
ok: true,
text: async () => "# 제목\n\n본문",
json: async () => ({ title: "제목", content: "# 제목\n\n본문" }),
});
});
});
Expand All @@ -125,7 +99,7 @@ describe("feedme spec tests", () => {
const markdownContent = "# 제목\n\n본문 내용";
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => markdownContent,
json: async () => ({ title: "제목", content: markdownContent }),
} as Response);

// userEvent.setup() 이후 clipboard mock 재설정 (userEvent가 clipboard를 교체하므로)
Expand Down Expand Up @@ -188,8 +162,8 @@ describe("feedme spec tests", () => {
const user = userEvent.setup();
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
text: async () => JSON.stringify({ error: "페이지에 접근할 수 없습니다" }),
status: 502,
json: async () => ({ error: "페이지에 접근할 수 없습니다" }),
} as Response);

render(<ContentExtractor />);
Expand All @@ -206,30 +180,6 @@ describe("feedme spec tests", () => {
});
});

// FEEDME-009
describe("FEEDME-009: 자막 없는 YouTube URL 에러", () => {
it("자막 없는 YouTube URL 입력 후 가져오기 클릭 시 자막을 찾을 수 없습니다 에러가 표시된다", async () => {
const user = userEvent.setup();
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
text: async () => JSON.stringify({ error: "자막을 찾을 수 없습니다" }),
} as Response);

render(<ContentExtractor />);

const input = screen.getByRole("textbox");
await user.type(input, "https://www.youtube.com/watch?v=no-captions");

const button = screen.getByRole("button", { name: "가져오기" });
await user.click(button);

await waitFor(() => {
expect(screen.getByText("자막을 찾을 수 없습니다")).toBeInTheDocument();
});
});
});

// FEEDME-010: When error is shown and user types in input → error text disappears
describe("FEEDME-010: 에러 표시 후 입력 시 에러 사라짐", () => {
it("에러가 표시된 상태에서 입력 필드에 타이핑하면 에러 텍스트가 사라진다", async () => {
Expand Down
83 changes: 31 additions & 52 deletions __tests__/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,24 @@ import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import ContentExtractor from "@/components/content-extractor";

function buildDefuddleText(
markdown: string,
meta: Record<string, string | undefined> = {}
): string {
const entries = Object.entries(meta).filter(([, v]) => v != null);
if (entries.length === 0) return markdown;
const lines = ["---"];
for (const [k, v] of entries) lines.push(`${k}: "${v}"`);
lines.push("---");
return `${lines.join("\n")}\n\n${markdown}`;
function jsonResponse(
data: Record<string, unknown>,
ok = true,
) {
return {
ok,
json: async () => data,
} as Response;
}

export async function renderWithContent(
markdown = "# Hello",
{ title, type }: { title?: string; type?: string } = {}
{ title }: { title?: string } = {},
) {
const user = userEvent.setup();
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => buildDefuddleText(markdown, { title }),
} as Response);
global.fetch = vi.fn().mockResolvedValue(
jsonResponse({ title, content: markdown }),
);

render(<ContentExtractor />);

Expand All @@ -32,27 +29,32 @@ export async function renderWithContent(
await user.click(screen.getByRole("button", { name: "가져오기" }));

await waitFor(() => {
expect(screen.getByRole("button", { name: "복사하기" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "복사하기" }),
).toBeInTheDocument();
});

return { user };
}

export async function renderWithContentAndOpenCollapsible(markdown = "# Hello") {
export async function renderWithContentAndOpenCollapsible(
markdown = "# Hello",
) {
const { user } = await renderWithContent(markdown);

const trigger = screen.getByRole("button", { name: /프롬프트 추가하기/i });
await user.click(trigger);

await waitFor(() => {
expect(screen.getByRole("textbox", { name: /프롬프트/i })).toBeInTheDocument();
expect(
screen.getByRole("textbox", { name: /프롬프트/i }),
).toBeInTheDocument();
});

return { user };
}

export async function renderWithWebpageResult({
thumbnail,
author,
domain,
title = "테스트 제목",
Expand All @@ -65,10 +67,13 @@ export async function renderWithWebpageResult({
content?: string;
}) {
const user = userEvent.setup();
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => buildDefuddleText(content, { title, author, domain }),
} as Response);
global.fetch = vi.fn().mockResolvedValue(
jsonResponse({
title,
content,
source: author || domain || undefined,
}),
);

render(<ContentExtractor />);

Expand All @@ -77,35 +82,9 @@ export async function renderWithWebpageResult({
await user.click(screen.getByRole("button", { name: "가져오기" }));

await waitFor(() => {
expect(screen.getByRole("button", { name: "복사하기" })).toBeInTheDocument();
});

return { user };
}

export async function renderWithYoutubeResult({
source = "Rick Astley",
title = "YouTube 테스트",
content = "# YouTube 콘텐츠",
}: {
source?: string;
title?: string;
content?: string;
} = {}) {
const user = userEvent.setup();
global.fetch = vi.fn().mockResolvedValue({
ok: true,
text: async () => buildDefuddleText(content, { title, author: source, site: "YouTube" }),
} as Response);

render(<ContentExtractor />);

const input = screen.getByRole("textbox");
await user.type(input, "https://www.youtube.com/watch?v=dQw4w9WgXcQ");
await user.click(screen.getByRole("button", { name: "가져오기" }));

await waitFor(() => {
expect(screen.getByRole("button", { name: "복사하기" })).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "복사하기" }),
).toBeInTheDocument();
});

return { user };
Expand Down
Loading
Loading