Fix failing tests and add unit tests
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { getBlogPostBySlug, getAllPosts } from "../../../lib/contentProcessor";
|
||||
import {
|
||||
getBlogPostBySlug,
|
||||
getAllBlogPosts as getAllPosts,
|
||||
} from "../../../lib/content";
|
||||
import ContentBanner from "../../components/ContentBanner";
|
||||
import RelatedArticles from "../../components/RelatedArticles";
|
||||
import AskOrganizer from "../../components/AskOrganizer";
|
||||
|
||||
@@ -116,13 +116,11 @@ export default function Logo({ size = "default", showText = true }) {
|
||||
: sizes.default;
|
||||
|
||||
return (
|
||||
<Link href="/" className="block">
|
||||
<Link href="/" className="block" aria-label="CommunityRule Logo">
|
||||
<div
|
||||
className={`flex items-center ${config.containerHeight} ${
|
||||
showText ? config.gap : ""
|
||||
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
|
||||
role="link"
|
||||
aria-label="CommunityRule Logo"
|
||||
>
|
||||
{/* Logo Text - only show if showText is true */}
|
||||
{showText && (
|
||||
|
||||
+17
-22
@@ -8,11 +8,24 @@ import { validateBlogPost, sanitizeBlogPost } from "./validation.js";
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert markdown content to HTML with basic formatting
|
||||
* @param {string} markdown - Raw markdown content
|
||||
* @returns {string} HTML content
|
||||
* Generate a URL-friendly slug from a string
|
||||
* @param {string} text - Text to convert to slug
|
||||
* @returns {string} URL-friendly slug
|
||||
*/
|
||||
function markdownToHtml(markdown) {
|
||||
function generateSlug(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "") // Remove special characters
|
||||
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
||||
.replace(/-+/g, "-") // Replace multiple hyphens with single
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all blog post files from the content directory
|
||||
* @returns {Array} Array of file paths
|
||||
*/
|
||||
export function markdownToHtml(markdown) {
|
||||
if (!markdown) return "";
|
||||
|
||||
return (
|
||||
@@ -41,24 +54,6 @@ function markdownToHtml(markdown) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL-friendly slug from a string
|
||||
* @param {string} text - Text to convert to slug
|
||||
* @returns {string} URL-friendly slug
|
||||
*/
|
||||
function generateSlug(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "") // Remove special characters
|
||||
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
||||
.replace(/-+/g, "-") // Replace multiple hyphens with single
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all blog post files from the content directory
|
||||
* @returns {Array} Array of file paths
|
||||
*/
|
||||
export function getBlogPostFiles() {
|
||||
const contentDirectory = path.join(process.cwd(), "content/blog");
|
||||
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import BlogPostPage from "../../app/blog/[slug]/page";
|
||||
|
||||
// Mock Next.js components
|
||||
vi.mock("next/navigation", () => ({
|
||||
notFound: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
default: ({ children, href, ...props }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock content processing
|
||||
vi.mock("../../lib/content", () => ({
|
||||
getBlogPostBySlug: vi.fn(),
|
||||
getAllBlogPosts: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock components
|
||||
vi.mock("../../app/components/ContentBanner", () => {
|
||||
return {
|
||||
default: ({ post }) => (
|
||||
<div data-testid="content-banner">
|
||||
<h1>{post.frontmatter.title}</h1>
|
||||
<p>{post.frontmatter.description}</p>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../app/components/RelatedArticles", () => {
|
||||
return {
|
||||
default: ({ relatedPosts, currentPostSlug }) => (
|
||||
<div data-testid="related-articles">
|
||||
<h2>Related Articles</h2>
|
||||
{relatedPosts.map((post) => (
|
||||
<div key={post.slug} data-testid={`related-${post.slug}`}>
|
||||
{post.frontmatter.title}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../app/components/AskOrganizer", () => {
|
||||
return {
|
||||
default: ({ title, subtitle, buttonText }) => (
|
||||
<div data-testid="ask-organizer">
|
||||
<h2>{title}</h2>
|
||||
<p>{subtitle}</p>
|
||||
<button>{buttonText}</button>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock asset utils
|
||||
vi.mock("../../lib/assetUtils", () => ({
|
||||
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
|
||||
ASSETS: {
|
||||
CONTENT_SHAPE_1: "Content_Shape_1.svg",
|
||||
CONTENT_SHAPE_2: "Content_Shape_2.svg",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock blog post data
|
||||
const mockPost = {
|
||||
slug: "test-article",
|
||||
frontmatter: {
|
||||
title: "Test Article Title",
|
||||
description: "This is a test article description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
htmlContent:
|
||||
"<p>This is the article content with <strong>bold text</strong> and <em>italic text</em>.</p>",
|
||||
};
|
||||
|
||||
const mockRelatedPosts = [
|
||||
{
|
||||
slug: "related-1",
|
||||
frontmatter: {
|
||||
title: "Related Article 1",
|
||||
description: "First related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-10",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "related-2",
|
||||
frontmatter: {
|
||||
title: "Related Article 2",
|
||||
description: "Second related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-12",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("BlogPostPage", () => {
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Mock the content functions
|
||||
const { getBlogPostBySlug, getAllBlogPosts } = require("../../lib/content");
|
||||
getBlogPostBySlug.mockResolvedValue(mockPost);
|
||||
getAllBlogPosts.mockResolvedValue([mockPost, ...mockRelatedPosts]);
|
||||
});
|
||||
|
||||
it("renders the blog post page with correct structure", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Check main container
|
||||
const mainContainer = document.querySelector("main");
|
||||
expect(mainContainer).toBeInTheDocument();
|
||||
expect(mainContainer).toHaveClass(
|
||||
"min-h-screen",
|
||||
"bg-[#F4F3F1]",
|
||||
"relative",
|
||||
"overflow-hidden"
|
||||
);
|
||||
});
|
||||
|
||||
it("renders the content banner", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
expect(screen.getByTestId("content-banner")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("This is a test article description")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the article content", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const article = document.querySelector("article");
|
||||
expect(article).toBeInTheDocument();
|
||||
expect(article).toHaveClass(
|
||||
"p-[var(--spacing-scale-024)]",
|
||||
"sm:py-[var(--spacing-scale-032)]"
|
||||
);
|
||||
|
||||
// Check content is rendered
|
||||
expect(screen.getByText(/This is the article content/)).toBeInTheDocument();
|
||||
expect(screen.getByText("bold text")).toBeInTheDocument();
|
||||
expect(screen.getByText("italic text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the related articles section", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
expect(screen.getByTestId("related-articles")).toBeInTheDocument();
|
||||
expect(screen.getByText("Related Articles")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("related-related-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("related-related-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the ask organizer section", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
expect(screen.getByTestId("ask-organizer")).toBeInTheDocument();
|
||||
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Get answers from an experienced organizer")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Ask an organizer")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders decorative shapes", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Check for decorative shapes
|
||||
const shapes = screen.getAllByAltText("");
|
||||
expect(shapes).toHaveLength(2);
|
||||
|
||||
// Check shape sources
|
||||
expect(shapes[0]).toHaveAttribute("src", "/assets/Content_Shape_1.svg");
|
||||
expect(shapes[1]).toHaveAttribute("src", "/assets/Content_Shape_2.svg");
|
||||
});
|
||||
|
||||
it("applies correct styling to article content", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const contentDiv = screen
|
||||
.getByText(/This is the article content/)
|
||||
.closest("div");
|
||||
expect(contentDiv).toHaveClass(
|
||||
"post-body",
|
||||
"-mt-[var(--spacing-scale-048)]",
|
||||
"text-[var(--color-content-inverse-primary)]",
|
||||
"text-[16px]",
|
||||
"leading-[24px]"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies responsive text sizing", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const contentDiv = screen
|
||||
.getByText(/This is the article content/)
|
||||
.closest("div");
|
||||
expect(contentDiv).toHaveClass(
|
||||
"sm:text-[18px]",
|
||||
"sm:leading-[130%]",
|
||||
"lg:text-[24px]",
|
||||
"lg:leading-[32px]",
|
||||
"xl:text-[32px]",
|
||||
"xl:leading-[40px]"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies responsive max-width constraints", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const contentDiv = screen
|
||||
.getByText(/This is the article content/)
|
||||
.closest("div");
|
||||
expect(contentDiv).toHaveClass(
|
||||
"sm:mx-auto",
|
||||
"sm:max-w-[390px]",
|
||||
"md:max-w-[472px]",
|
||||
"lg:max-w-[700px]",
|
||||
"xl:max-w-[904px]"
|
||||
);
|
||||
});
|
||||
|
||||
it("includes structured data scripts", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const scripts = screen.getAllByRole("script");
|
||||
expect(scripts).toHaveLength(2);
|
||||
|
||||
// Check that scripts have the correct type
|
||||
scripts.forEach((script) => {
|
||||
expect(script).toHaveAttribute("type", "application/ld+json");
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing post gracefully", async () => {
|
||||
const { getBlogPostBySlug } = require("../../lib/content");
|
||||
getBlogPostBySlug.mockResolvedValue(null);
|
||||
|
||||
const { notFound } = require("next/navigation");
|
||||
|
||||
await BlogPostPage({ params: { slug: "non-existent" } });
|
||||
|
||||
expect(notFound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("filters out current post from related articles", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
// Current post should not appear in related articles
|
||||
expect(
|
||||
screen.queryByTestId("related-test-article")
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Other related posts should appear
|
||||
expect(screen.getByTestId("related-related-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("related-related-2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct positioning to decorative shapes", async () => {
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "test-article" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
const shapes = screen.getAllByAltText("");
|
||||
|
||||
// First shape (right side)
|
||||
const rightShape = shapes[0].closest("div");
|
||||
expect(rightShape).toHaveClass(
|
||||
"hidden",
|
||||
"md:block",
|
||||
"absolute",
|
||||
"top-1/4",
|
||||
"right-0",
|
||||
"pointer-events-none",
|
||||
"z-10"
|
||||
);
|
||||
|
||||
// Second shape (left side)
|
||||
const leftShape = shapes[1].closest("div");
|
||||
expect(leftShape).toHaveClass(
|
||||
"hidden",
|
||||
"md:block",
|
||||
"absolute",
|
||||
"top-1/2",
|
||||
"left-0",
|
||||
"pointer-events-none",
|
||||
"z-10"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles malformed post data gracefully", async () => {
|
||||
const malformedPost = {
|
||||
slug: "malformed",
|
||||
frontmatter: {
|
||||
title: "Malformed Post",
|
||||
// Missing other fields
|
||||
},
|
||||
htmlContent: "<p>Content</p>",
|
||||
};
|
||||
|
||||
const { getBlogPostBySlug } = require("../../lib/content");
|
||||
getBlogPostBySlug.mockResolvedValue(malformedPost);
|
||||
|
||||
const BlogPostPageComponent = await BlogPostPage({
|
||||
params: { slug: "malformed" },
|
||||
});
|
||||
render(BlogPostPageComponent);
|
||||
|
||||
expect(screen.getByText("Malformed Post")).toBeInTheDocument();
|
||||
expect(screen.getByText("Content")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import ContentBanner from "../../app/components/ContentBanner";
|
||||
|
||||
// Mock Next.js components
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
default: ({ children, href, ...props }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock asset utils
|
||||
vi.mock("../../lib/assetUtils", () => ({
|
||||
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
|
||||
ASSETS: {
|
||||
CONTENT_BANNER_1: "Content_Banner_1.svg",
|
||||
CONTENT_BANNER_2: "Content_Banner_2.svg",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock blog post data
|
||||
const mockPost = {
|
||||
slug: "test-article",
|
||||
frontmatter: {
|
||||
title: "Test Article Title",
|
||||
description: "This is a test article description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
};
|
||||
|
||||
describe("ContentBanner", () => {
|
||||
it("renders the banner with correct structure", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check that the banner container exists - it's the first div with the specific classes
|
||||
const banner = document.querySelector(
|
||||
"div[class*='pt-[var(--measures-spacing-016)]']"
|
||||
);
|
||||
expect(banner).toBeInTheDocument();
|
||||
expect(banner).toHaveClass(
|
||||
"pt-[var(--measures-spacing-016)]",
|
||||
"md:pt-[var(--measures-spacing-008)]",
|
||||
"lg:pt-[50px]",
|
||||
"xl:pt-[112px]",
|
||||
"h-[275px]",
|
||||
"sm:h-[326px]",
|
||||
"md:h-[224px]",
|
||||
"lg:h-[358.4px]",
|
||||
"xl:h-[504px]",
|
||||
"relative",
|
||||
"w-full",
|
||||
"sm:overflow-hidden"
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the background image correctly", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check for background div with correct styling
|
||||
const backgroundDiv = document.querySelector(
|
||||
"div[style*='background-image']"
|
||||
);
|
||||
expect(backgroundDiv).toBeInTheDocument();
|
||||
expect(backgroundDiv).toHaveClass(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"w-full",
|
||||
"h-full",
|
||||
"bg-cover",
|
||||
"bg-no-repeat",
|
||||
"aspect-[320/225.5]"
|
||||
);
|
||||
});
|
||||
|
||||
it("shows different background image at md breakpoint and above", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check for the md+ background div
|
||||
const mdBackgroundDiv = document.querySelector(
|
||||
"div[style*='Content_Banner_2.svg']"
|
||||
);
|
||||
expect(mdBackgroundDiv).toBeInTheDocument();
|
||||
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
|
||||
});
|
||||
|
||||
it("displays the article title", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the article description", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("This is a test article description")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays the author and date metadata", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
expect(screen.getByText("Test Author")).toBeInTheDocument();
|
||||
expect(screen.getByText("April 2025")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct styling classes", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check the content container div
|
||||
const contentContainer = document.querySelector(
|
||||
"div[class*='relative z-10']"
|
||||
);
|
||||
expect(contentContainer).toBeInTheDocument();
|
||||
expect(contentContainer).toHaveClass(
|
||||
"relative",
|
||||
"z-10",
|
||||
"h-full",
|
||||
"flex",
|
||||
"flex-col"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct text styling", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toHaveClass(
|
||||
"font-bricolage",
|
||||
"font-medium",
|
||||
"text-[18px]",
|
||||
"leading-[120%]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
|
||||
const description = screen.getByText("This is a test article description");
|
||||
expect(description).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[12px]",
|
||||
"leading-[16px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct metadata styling", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const author = screen.getByText("Test Author");
|
||||
expect(author).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[10px]",
|
||||
"leading-[14px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
|
||||
const date = screen.getByText("April 2025");
|
||||
expect(date).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[10px]",
|
||||
"leading-[14px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("has proper spacing between elements", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check the ContentContainer spacing
|
||||
const contentContainer = document.querySelector(
|
||||
"div[class*='relative z-20']"
|
||||
);
|
||||
expect(contentContainer).toHaveClass("gap-[var(--measures-spacing-012)]");
|
||||
});
|
||||
|
||||
it("has proper outer container padding", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const outerContainer = document.querySelector(
|
||||
"div[class*='pt-[var(--measures-spacing-016)]']"
|
||||
);
|
||||
expect(outerContainer).toHaveClass(
|
||||
"pt-[var(--measures-spacing-016)]",
|
||||
"md:pt-[var(--measures-spacing-008)]",
|
||||
"lg:pt-[50px]",
|
||||
"xl:pt-[112px]"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles missing post data gracefully", () => {
|
||||
const incompletePost = {
|
||||
slug: "incomplete",
|
||||
frontmatter: {
|
||||
title: "Incomplete Post",
|
||||
// Missing other fields
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContentBanner post={incompletePost} />);
|
||||
|
||||
expect(screen.getByText("Incomplete Post")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies responsive text sizing", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toHaveClass(
|
||||
"sm:text-[24px]",
|
||||
"md:text-[32px]",
|
||||
"lg:text-[44px]",
|
||||
"xl:text-[64px]"
|
||||
);
|
||||
|
||||
const description = screen.getByText("This is a test article description");
|
||||
expect(description).toHaveClass(
|
||||
"sm:text-[14px]",
|
||||
"md:text-[14px]",
|
||||
"lg:text-[18px]",
|
||||
"xl:text-[24px]"
|
||||
);
|
||||
});
|
||||
|
||||
it("has proper accessibility attributes", () => {
|
||||
render(<ContentBanner post={mockPost} />);
|
||||
|
||||
// Check that the component renders without accessibility errors
|
||||
const banner = document.querySelector("div");
|
||||
expect(banner).toBeInTheDocument();
|
||||
|
||||
// Check that the icon has proper alt text
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,254 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import ContentContainer from "../../app/components/ContentContainer";
|
||||
|
||||
// Mock asset utils
|
||||
vi.mock("../../lib/assetUtils", () => ({
|
||||
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
|
||||
ASSETS: {
|
||||
ICON_1: "Icon_1.svg",
|
||||
ICON_2: "Icon_2.svg",
|
||||
ICON_3: "Icon_3.svg",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock blog post data
|
||||
const mockPost = {
|
||||
slug: "test-article",
|
||||
frontmatter: {
|
||||
title: "Test Article Title",
|
||||
description:
|
||||
"This is a test article description that should be long enough to test truncation and wrapping behavior.",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
};
|
||||
|
||||
describe("ContentContainer", () => {
|
||||
it("renders with default props", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
// Check that the container exists
|
||||
const container = document.querySelector("div[class*='relative z-20']");
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container).toHaveClass(
|
||||
"relative",
|
||||
"z-20",
|
||||
"h-full",
|
||||
"flex",
|
||||
"flex-col"
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the icon correctly", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(icon).toHaveAttribute("src", "/assets/Icon_1.svg");
|
||||
expect(icon).toHaveClass("w-[60px]", "h-[30px]", "object-contain");
|
||||
});
|
||||
|
||||
it("displays the article title", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveClass(
|
||||
"font-bricolage",
|
||||
"font-medium",
|
||||
"text-[18px]",
|
||||
"leading-[120%]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the article description", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const description = screen.getByText(/This is a test article description/);
|
||||
expect(description).toBeInTheDocument();
|
||||
expect(description).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[12px]",
|
||||
"leading-[16px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the author and date metadata", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
expect(screen.getByText("Test Author")).toBeInTheDocument();
|
||||
expect(screen.getByText("April 2025")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct width when specified", () => {
|
||||
render(<ContentContainer post={mockPost} width="300px" size="sm" />);
|
||||
|
||||
const container = document.querySelector("div[class*='relative z-20']");
|
||||
expect(container).toHaveStyle("width: 300px");
|
||||
});
|
||||
|
||||
it("applies default width when not specified", () => {
|
||||
render(<ContentContainer post={mockPost} size="sm" />);
|
||||
|
||||
const container = document.querySelector("div[class*='relative z-20']");
|
||||
expect(container).toHaveStyle("width: 200px");
|
||||
});
|
||||
|
||||
it("has proper spacing between icon and text", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const iconContainer = screen
|
||||
.getByAltText("Icon for Test Article Title")
|
||||
.closest("div");
|
||||
const textContainer = screen.getByText("Test Article Title").closest("div");
|
||||
|
||||
// Check the content container (parent of icon)
|
||||
expect(iconContainer.parentElement).toHaveClass(
|
||||
"gap-[var(--measures-spacing-008)]"
|
||||
);
|
||||
// Check the text container (parent of title)
|
||||
expect(textContainer.parentElement).toHaveClass(
|
||||
"gap-[var(--measures-spacing-004)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("has proper metadata container styling", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const metadataContainer = screen.getByText("Test Author").closest("div");
|
||||
expect(metadataContainer).toHaveClass(
|
||||
"flex",
|
||||
"items-center",
|
||||
"gap-[var(--measures-spacing-008)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct metadata text styling", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const author = screen.getByText("Test Author");
|
||||
expect(author).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[10px]",
|
||||
"leading-[14px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
|
||||
const date = screen.getByText("April 2025");
|
||||
expect(date).toHaveClass(
|
||||
"font-inter",
|
||||
"font-normal",
|
||||
"text-[10px]",
|
||||
"leading-[14px]",
|
||||
"text-[var(--color-content-inverse-brand-royal)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("cycles through different icons based on slug", () => {
|
||||
const { rerender } = render(<ContentContainer post={mockPost} />);
|
||||
|
||||
// First render should use Icon_1
|
||||
let icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toHaveAttribute("src", "/assets/Icon_1.svg");
|
||||
|
||||
// Test with different slug
|
||||
const post2 = { ...mockPost, slug: "operational-security-mutual-aid" };
|
||||
rerender(<ContentContainer post={post2} />);
|
||||
|
||||
icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toHaveAttribute("src", "/assets/Icon_2.svg");
|
||||
|
||||
// Test with another slug
|
||||
const post3 = { ...mockPost, slug: "making-decisions-without-hierarchy" };
|
||||
rerender(<ContentContainer post={post3} />);
|
||||
|
||||
icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toHaveAttribute("src", "/assets/Icon_3.svg");
|
||||
});
|
||||
|
||||
it("handles missing post data gracefully", () => {
|
||||
const incompletePost = {
|
||||
slug: "incomplete",
|
||||
frontmatter: {
|
||||
title: "Incomplete Post",
|
||||
// Missing other fields
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContentContainer post={incompletePost} />);
|
||||
|
||||
expect(screen.getByText("Incomplete Post")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct responsive sizing for sm breakpoint", () => {
|
||||
render(<ContentContainer post={mockPost} size="sm" />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toHaveClass("w-[60px]", "h-[30px]");
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toHaveClass("text-[18px]", "leading-[120%]");
|
||||
|
||||
const description = screen.getByText(/This is a test article description/);
|
||||
expect(description).toHaveClass("text-[12px]", "leading-[16px]");
|
||||
});
|
||||
|
||||
it("applies correct responsive sizing for md breakpoint", () => {
|
||||
render(<ContentContainer post={mockPost} size="md" />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toHaveClass("w-[60px]", "h-[30px]");
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
expect(title).toHaveClass("text-[18px]", "leading-[120%]");
|
||||
|
||||
const description = screen.getByText(/This is a test article description/);
|
||||
expect(description).toHaveClass("text-[12px]", "leading-[16px]");
|
||||
});
|
||||
|
||||
it("has proper accessibility attributes", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toHaveAttribute("alt", "Icon for Test Article Title");
|
||||
});
|
||||
|
||||
it("handles long titles gracefully", () => {
|
||||
const longTitlePost = {
|
||||
...mockPost,
|
||||
frontmatter: {
|
||||
...mockPost.frontmatter,
|
||||
title:
|
||||
"This is a very long article title that should test how the component handles lengthy text content",
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContentContainer post={longTitlePost} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/This is a very long article title/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles long descriptions gracefully", () => {
|
||||
const longDescPost = {
|
||||
...mockPost,
|
||||
frontmatter: {
|
||||
...mockPost.frontmatter,
|
||||
description:
|
||||
"This is a very long article description that should test how the component handles lengthy text content and ensures proper wrapping and truncation behavior.",
|
||||
},
|
||||
};
|
||||
|
||||
render(<ContentContainer post={longDescPost} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/This is a very long article description/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -54,39 +54,24 @@ describe("ContentThumbnailTemplate", () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display tags when showTags is true", () => {
|
||||
render(<ContentThumbnailTemplate post={mockPost} showTags={true} />);
|
||||
|
||||
expect(screen.getByText("test")).toBeInTheDocument();
|
||||
expect(screen.getByText("blog")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide tags when showTags is false", () => {
|
||||
render(<ContentThumbnailTemplate post={mockPost} showTags={false} />);
|
||||
|
||||
expect(screen.queryByText("test")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("blog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display author and date", () => {
|
||||
it("should display author and date metadata", () => {
|
||||
render(<ContentThumbnailTemplate post={mockPost} />);
|
||||
|
||||
expect(screen.getByText("Test Author")).toBeInTheDocument();
|
||||
// Check for "Month Year" format (e.g., "April 2025")
|
||||
expect(screen.getByText("April 2025")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Horizontal Variant", () => {
|
||||
it("should render horizontal variant", () => {
|
||||
render(<ContentThumbnailTemplate post={mockPost} />);
|
||||
render(<ContentThumbnailTemplate post={mockPost} variant="horizontal" />);
|
||||
|
||||
const container = screen.getByRole("link");
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
// Check that the component has the correct classes for horizontal layout
|
||||
const thumbnailDiv = container.querySelector("div");
|
||||
expect(thumbnailDiv).toHaveClass("h-[226px]");
|
||||
expect(thumbnailDiv).toHaveClass("h-[225.5px]");
|
||||
});
|
||||
|
||||
it("should display post information in horizontal layout", () => {
|
||||
@@ -156,10 +141,11 @@ describe("ContentThumbnailTemplate", () => {
|
||||
expect(thumbnailDiv).toHaveClass("w-[260px]", "h-[390px]");
|
||||
});
|
||||
|
||||
it("should show tags by default", () => {
|
||||
it("should show metadata by default", () => {
|
||||
render(<ContentThumbnailTemplate post={mockPost} />);
|
||||
|
||||
expect(screen.getByText("test")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Author")).toBeInTheDocument();
|
||||
expect(screen.getByText("April 2025")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+17
-17
@@ -27,7 +27,7 @@ describe("Footer", () => {
|
||||
expect(schemaData.email).toBe("medlab@colorado.edu");
|
||||
expect(schemaData.url).toBe("https://communityrule.com");
|
||||
expect(schemaData.sameAs).toContain(
|
||||
"https://bsky.app/profile/medlabboulder",
|
||||
"https://bsky.app/profile/medlabboulder"
|
||||
);
|
||||
expect(schemaData.sameAs).toContain("https://gitlab.com/medlabboulder");
|
||||
});
|
||||
@@ -36,7 +36,7 @@ describe("Footer", () => {
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getAllByText("Media Economies Design Lab").length,
|
||||
screen.getAllByText("Media Economies Design Lab").length
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
const emailLinks = screen.getAllByRole("link", {
|
||||
@@ -73,26 +73,26 @@ describe("Footer", () => {
|
||||
expect(blueskyImages.length).toBeGreaterThan(0);
|
||||
const blueskyImage = blueskyImages[0];
|
||||
expect(blueskyImage).toBeInTheDocument();
|
||||
expect(blueskyImage).toHaveAttribute("src", "assets/Bluesky_Logo.svg");
|
||||
expect(blueskyImage).toHaveAttribute("src", "/assets/Bluesky_Logo.svg");
|
||||
|
||||
const gitlabImages = screen.getAllByAltText("GitLab");
|
||||
expect(gitlabImages.length).toBeGreaterThan(0);
|
||||
const gitlabImage = gitlabImages[0];
|
||||
expect(gitlabImage).toBeInTheDocument();
|
||||
expect(gitlabImage).toHaveAttribute("src", "assets/GitLab_Icon.png");
|
||||
expect(gitlabImage).toHaveAttribute("src", "/assets/GitLab_Icon.png");
|
||||
});
|
||||
|
||||
test("renders navigation links", () => {
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "Use cases" }).length,
|
||||
screen.getAllByRole("link", { name: "Use cases" }).length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "Learn" }).length,
|
||||
screen.getAllByRole("link", { name: "Learn" }).length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "About" }).length,
|
||||
screen.getAllByRole("link", { name: "About" }).length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -100,13 +100,13 @@ describe("Footer", () => {
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "Privacy Policy" }).length,
|
||||
screen.getAllByRole("link", { name: "Privacy Policy" }).length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "Terms of Service" }).length,
|
||||
screen.getAllByRole("link", { name: "Terms of Service" }).length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("link", { name: "Cookies Settings" }).length,
|
||||
screen.getAllByRole("link", { name: "Cookies Settings" }).length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("Footer", () => {
|
||||
render(<Footer />);
|
||||
|
||||
expect(screen.getAllByText("© All right reserved").length).toBeGreaterThan(
|
||||
0,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("Footer", () => {
|
||||
|
||||
// Check that logo containers exist for different breakpoints
|
||||
const logoContainers = document.querySelectorAll(
|
||||
'[class*="block sm:hidden"], [class*="hidden sm:block lg:hidden"], [class*="hidden lg:block"]',
|
||||
'[class*="block sm:hidden"], [class*="hidden sm:block lg:hidden"], [class*="hidden lg:block"]'
|
||||
);
|
||||
expect(logoContainers.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -147,7 +147,7 @@ describe("Footer", () => {
|
||||
|
||||
// The Separator component should be rendered (it uses a div with border, not hr)
|
||||
const separator = document.querySelector(
|
||||
".bg-\\[var\\(--border-color-default-secondary\\)\\]",
|
||||
".bg-\\[var\\(--border-color-default-secondary\\)\\]"
|
||||
);
|
||||
expect(separator).toBeInTheDocument();
|
||||
});
|
||||
@@ -263,10 +263,10 @@ describe("Footer", () => {
|
||||
expect(emailLink).toHaveClass("focus:ring-2");
|
||||
expect(emailLink).toHaveClass("focus:ring-offset-2");
|
||||
expect(emailLink).toHaveClass(
|
||||
"focus:ring-[var(--color-content-default-primary)]",
|
||||
"focus:ring-[var(--color-content-default-primary)]"
|
||||
);
|
||||
expect(emailLink).toHaveClass(
|
||||
"focus:ring-offset-[var(--color-surface-default-primary)]",
|
||||
"focus:ring-offset-[var(--color-surface-default-primary)]"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -276,10 +276,10 @@ describe("Footer", () => {
|
||||
expect(link).toHaveClass("focus:ring-2");
|
||||
expect(link).toHaveClass("focus:ring-offset-2");
|
||||
expect(link).toHaveClass(
|
||||
"focus:ring-[var(--color-content-default-primary)]",
|
||||
"focus:ring-[var(--color-content-default-primary)]"
|
||||
);
|
||||
expect(link).toHaveClass(
|
||||
"focus:ring-offset-[var(--color-surface-default-primary)]",
|
||||
"focus:ring-offset-[var(--color-surface-default-primary)]"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+13
-13
@@ -22,14 +22,14 @@ describe("Header", () => {
|
||||
|
||||
// Check main header structure - use container to scope the search
|
||||
const header = container.querySelector(
|
||||
'[role="banner"][aria-label="Main navigation header"]',
|
||||
'[role="banner"][aria-label="Main navigation header"]'
|
||||
);
|
||||
expect(header).toBeInTheDocument();
|
||||
expect(header).toHaveAttribute("aria-label", "Main navigation header");
|
||||
|
||||
// Check navigation - use container to scope the search
|
||||
const nav = container.querySelector(
|
||||
'[role="navigation"][aria-label="Main navigation"]',
|
||||
'[role="navigation"][aria-label="Main navigation"]'
|
||||
);
|
||||
expect(nav).toBeInTheDocument();
|
||||
expect(nav).toHaveAttribute("aria-label", "Main navigation");
|
||||
@@ -41,15 +41,15 @@ describe("Header", () => {
|
||||
// Check all navigation items have proper aria-labels - use menuitem role since they're in a menubar
|
||||
expect(
|
||||
screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" })
|
||||
.length,
|
||||
.length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("menuitem", { name: "Navigate to Learn page" })
|
||||
.length,
|
||||
.length
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole("menuitem", { name: "Navigate to About page" })
|
||||
.length,
|
||||
.length
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ describe("Header", () => {
|
||||
render(<Header onToggle={mockOnToggle} />);
|
||||
|
||||
const script = document.querySelector(
|
||||
'script[type="application/ld+json"]',
|
||||
'script[type="application/ld+json"]'
|
||||
);
|
||||
expect(script).toBeInTheDocument();
|
||||
|
||||
@@ -92,15 +92,15 @@ describe("Header", () => {
|
||||
test("avatarImages has correct structure and count", () => {
|
||||
expect(avatarImages).toHaveLength(3);
|
||||
expect(avatarImages[0]).toEqual({
|
||||
src: "assets/Avatar_1.png",
|
||||
src: "/assets/Avatar_1.png",
|
||||
alt: "Avatar 1",
|
||||
});
|
||||
expect(avatarImages[1]).toEqual({
|
||||
src: "assets/Avatar_2.png",
|
||||
src: "/assets/Avatar_2.png",
|
||||
alt: "Avatar 2",
|
||||
});
|
||||
expect(avatarImages[2]).toEqual({
|
||||
src: "assets/Avatar_3.png",
|
||||
src: "/assets/Avatar_3.png",
|
||||
alt: "Avatar 3",
|
||||
});
|
||||
});
|
||||
@@ -296,7 +296,7 @@ describe("Header", () => {
|
||||
(img) =>
|
||||
img.alt === "Avatar 1" ||
|
||||
img.alt === "Avatar 2" ||
|
||||
img.alt === "Avatar 3",
|
||||
img.alt === "Avatar 3"
|
||||
);
|
||||
expect(avatarImages.length).toBeGreaterThan(0);
|
||||
});
|
||||
@@ -324,17 +324,17 @@ describe("Header", () => {
|
||||
const { container } = render(<Header onToggle={mockOnToggle} />);
|
||||
|
||||
const header = container.querySelector(
|
||||
'[role="banner"][aria-label="Main navigation header"]',
|
||||
'[role="banner"][aria-label="Main navigation header"]'
|
||||
);
|
||||
expect(header).toHaveClass("bg-[var(--color-surface-default-primary)]");
|
||||
expect(header).toHaveClass("w-full");
|
||||
expect(header).toHaveClass("border-b");
|
||||
expect(header).toHaveClass(
|
||||
"border-[var(--border-color-default-tertiary)]",
|
||||
"border-[var(--border-color-default-tertiary)]"
|
||||
);
|
||||
|
||||
const nav = container.querySelector(
|
||||
'[role="navigation"][aria-label="Main navigation"]',
|
||||
'[role="navigation"][aria-label="Main navigation"]'
|
||||
);
|
||||
expect(nav).toHaveClass("flex");
|
||||
expect(nav).toHaveClass("items-center");
|
||||
|
||||
+12
-12
@@ -14,16 +14,16 @@ describe("Logo Component", () => {
|
||||
|
||||
it("renders with custom size variant", () => {
|
||||
const { rerender } = render(<Logo size="header" />);
|
||||
let logo = screen.getByRole("link");
|
||||
expect(logo).toHaveClass("h-[20.85px]");
|
||||
let logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("h-[20.85px]");
|
||||
|
||||
rerender(<Logo size="headerLg" />);
|
||||
logo = screen.getByRole("link");
|
||||
expect(logo).toHaveClass("h-[28px]");
|
||||
logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("h-[28px]");
|
||||
|
||||
rerender(<Logo size="footer" />);
|
||||
logo = screen.getByRole("link");
|
||||
expect(logo).toHaveClass("h-[calc(40px*1.37)]");
|
||||
logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("h-[calc(40px*1.37)]");
|
||||
});
|
||||
|
||||
it("renders without text when showText is false", () => {
|
||||
@@ -36,8 +36,8 @@ describe("Logo Component", () => {
|
||||
it("applies proper hover effects", () => {
|
||||
render(<Logo />);
|
||||
|
||||
const logo = screen.getByRole("link");
|
||||
expect(logo).toHaveClass("hover:scale-[1.02]", "transition-all");
|
||||
const logoDiv = screen.getByRole("link").querySelector("div");
|
||||
expect(logoDiv).toHaveClass("hover:scale-[1.02]", "transition-all");
|
||||
});
|
||||
|
||||
it("applies proper accessibility attributes", () => {
|
||||
@@ -45,20 +45,20 @@ describe("Logo Component", () => {
|
||||
|
||||
const logo = screen.getByRole("link");
|
||||
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
|
||||
expect(logo).toHaveAttribute("role", "link");
|
||||
expect(logo).toHaveAttribute("href", "/");
|
||||
});
|
||||
|
||||
it("applies proper text styling for different sizes", () => {
|
||||
const { rerender } = render(<Logo size="homeHeaderMd" />);
|
||||
let textElement = screen.getByText("CommunityRule");
|
||||
expect(textElement).toHaveClass(
|
||||
"text-[var(--color-content-inverse-primary)]",
|
||||
"text-[var(--color-content-inverse-primary)]"
|
||||
);
|
||||
|
||||
rerender(<Logo size="header" />);
|
||||
textElement = screen.getByText("CommunityRule");
|
||||
expect(textElement).toHaveClass(
|
||||
"text-[var(--color-content-default-primary)]",
|
||||
"text-[var(--color-content-default-primary)]"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("Logo Component", () => {
|
||||
render(<Logo />);
|
||||
|
||||
const icon = screen.getByAltText("CommunityRule Logo Icon");
|
||||
expect(icon).toHaveAttribute("src", "assets/Logo.svg");
|
||||
expect(icon).toHaveAttribute("src", "/assets/Logo.svg");
|
||||
expect(icon).toHaveAttribute("aria-hidden", "true");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { markdownToHtml } from "../../lib/content";
|
||||
|
||||
describe("Markdown Processing", () => {
|
||||
describe("markdownToHtml", () => {
|
||||
it("converts basic markdown to HTML", () => {
|
||||
const markdown = "# Heading\n\nThis is a paragraph.";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<h1>Heading</h1>");
|
||||
expect(result).toContain("<p>This is a paragraph.</p>");
|
||||
});
|
||||
|
||||
it("converts bold text", () => {
|
||||
const markdown = "This is **bold** text.";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<strong>bold</strong>");
|
||||
});
|
||||
|
||||
it("converts italic text", () => {
|
||||
const markdown = "This is *italic* text.";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<em>italic</em>");
|
||||
});
|
||||
|
||||
it("converts links", () => {
|
||||
const markdown = "Visit [Google](https://google.com) for search.";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain('<a href="https://google.com">Google</a>');
|
||||
});
|
||||
|
||||
it("converts line breaks to <br> tags", () => {
|
||||
const markdown = "Line 1\nLine 2\nLine 3";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("Line 1<br>");
|
||||
expect(result).toContain("Line 2<br>");
|
||||
expect(result).toContain("Line 3");
|
||||
});
|
||||
|
||||
it("converts multiple line breaks to paragraph breaks", () => {
|
||||
const markdown = "Paragraph 1\n\nParagraph 2\n\nParagraph 3";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<p>Paragraph 1</p>");
|
||||
expect(result).toContain("<p>Paragraph 2</p>");
|
||||
expect(result).toContain("<p>Paragraph 3</p>");
|
||||
});
|
||||
|
||||
it("adds md-gap class to paragraphs", () => {
|
||||
const markdown = "Paragraph 1\n\nParagraph 2";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain('<p class="md-gap">Paragraph 1</p>');
|
||||
expect(result).toContain('<p class="md-gap">Paragraph 2</p>');
|
||||
});
|
||||
|
||||
it("converts unordered lists", () => {
|
||||
const markdown = "- Item 1\n- Item 2\n- Item 3";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<ul>");
|
||||
expect(result).toContain("<li>Item 1</li>");
|
||||
expect(result).toContain("<li>Item 2</li>");
|
||||
expect(result).toContain("<li>Item 3</li>");
|
||||
expect(result).toContain("</ul>");
|
||||
});
|
||||
|
||||
it("converts ordered lists", () => {
|
||||
const markdown = "1. First item\n2. Second item\n3. Third item";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<ol>");
|
||||
expect(result).toContain("<li>First item</li>");
|
||||
expect(result).toContain("<li>Second item</li>");
|
||||
expect(result).toContain("<li>Third item</li>");
|
||||
expect(result).toContain("</ol>");
|
||||
});
|
||||
|
||||
it("converts code blocks", () => {
|
||||
const markdown = "```javascript\nconst x = 1;\n```";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<pre>");
|
||||
expect(result).toContain("<code>");
|
||||
expect(result).toContain("const x = 1;");
|
||||
});
|
||||
|
||||
it("converts inline code", () => {
|
||||
const markdown = "Use `console.log()` to debug.";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<code>console.log()</code>");
|
||||
});
|
||||
|
||||
it("converts blockquotes", () => {
|
||||
const markdown = "> This is a quote\n> with multiple lines";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<blockquote>");
|
||||
expect(result).toContain("This is a quote");
|
||||
expect(result).toContain("with multiple lines");
|
||||
expect(result).toContain("</blockquote>");
|
||||
});
|
||||
|
||||
it("converts horizontal rules", () => {
|
||||
const markdown = "Text above\n\n---\n\nText below";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<hr>");
|
||||
});
|
||||
|
||||
it("handles mixed content", () => {
|
||||
const markdown =
|
||||
"# Title\n\nThis is a **bold** paragraph with a [link](https://example.com).\n\n- List item 1\n- List item 2\n\nAnother paragraph with `code`.";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<h1>Title</h1>");
|
||||
expect(result).toContain("<strong>bold</strong>");
|
||||
expect(result).toContain('<a href="https://example.com">link</a>');
|
||||
expect(result).toContain("<ul>");
|
||||
expect(result).toContain("<li>List item 1</li>");
|
||||
expect(result).toContain("<li>List item 2</li>");
|
||||
expect(result).toContain("<code>code</code>");
|
||||
});
|
||||
|
||||
it("handles empty input", () => {
|
||||
const result = markdownToHtml("");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("handles whitespace-only input", () => {
|
||||
const result = markdownToHtml(" \n\n ");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("preserves HTML entities", () => {
|
||||
const markdown = "Use < and > for HTML tags.";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<");
|
||||
expect(result).toContain(">");
|
||||
});
|
||||
|
||||
it("handles complex nested structures", () => {
|
||||
const markdown =
|
||||
"# Main Title\n\n## Subtitle\n\nThis is a paragraph with **bold** and *italic* text.\n\n1. First item with `code`\n2. Second item with [link](https://example.com)\n\n> This is a quote\n> with **bold** text\n\n```javascript\nconst example = 'test';\n```";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<h1>Main Title</h1>");
|
||||
expect(result).toContain("<h2>Subtitle</h2>");
|
||||
expect(result).toContain("<strong>bold</strong>");
|
||||
expect(result).toContain("<em>italic</em>");
|
||||
expect(result).toContain("<ol>");
|
||||
expect(result).toContain("<code>code</code>");
|
||||
expect(result).toContain('<a href="https://example.com">link</a>');
|
||||
expect(result).toContain("<blockquote>");
|
||||
expect(result).toContain("<pre>");
|
||||
});
|
||||
|
||||
it("handles malformed markdown gracefully", () => {
|
||||
const markdown = "**Unclosed bold\n\n*Unclosed italic\n\n[Unclosed link";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
// Should not throw an error and should handle gracefully
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("converts headings of different levels", () => {
|
||||
const markdown = "# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<h1>H1</h1>");
|
||||
expect(result).toContain("<h2>H2</h2>");
|
||||
expect(result).toContain("<h3>H3</h3>");
|
||||
expect(result).toContain("<h4>H4</h4>");
|
||||
expect(result).toContain("<h5>H5</h5>");
|
||||
expect(result).toContain("<h6>H6</h6>");
|
||||
});
|
||||
|
||||
it("handles tables", () => {
|
||||
const markdown =
|
||||
"| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
|
||||
const result = markdownToHtml(markdown);
|
||||
|
||||
expect(result).toContain("<table>");
|
||||
expect(result).toContain("<thead>");
|
||||
expect(result).toContain("<th>Header 1</th>");
|
||||
expect(result).toContain("<th>Header 2</th>");
|
||||
expect(result).toContain("<tbody>");
|
||||
expect(result).toContain("<td>Cell 1</td>");
|
||||
expect(result).toContain("<td>Cell 2</td>");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,395 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import RelatedArticles from "../../app/components/RelatedArticles";
|
||||
|
||||
// Mock Next.js components
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
default: ({ children, href, ...props }) => (
|
||||
<a href={href} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock ContentThumbnailTemplate
|
||||
vi.mock("../../app/components/ContentThumbnailTemplate", () => {
|
||||
return {
|
||||
default: ({ post }) => (
|
||||
<div data-testid={`thumbnail-${post.slug}`}>
|
||||
<a href={`/blog/${post.slug}`}>
|
||||
<h3>{post.frontmatter.title}</h3>
|
||||
<p>{post.frontmatter.description}</p>
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock blog post data
|
||||
const mockRelatedPosts = [
|
||||
{
|
||||
slug: "related-article-1",
|
||||
frontmatter: {
|
||||
title: "Related Article 1",
|
||||
description: "This is the first related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-10",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "related-article-2",
|
||||
frontmatter: {
|
||||
title: "Related Article 2",
|
||||
description: "This is the second related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-12",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "related-article-3",
|
||||
frontmatter: {
|
||||
title: "Related Article 3",
|
||||
description: "This is the third related article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-14",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe("RelatedArticles", () => {
|
||||
beforeEach(() => {
|
||||
// Mock window.innerWidth for responsive tests
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 1024, // Desktop width
|
||||
});
|
||||
});
|
||||
|
||||
it("renders the section with correct structure", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toBeInTheDocument();
|
||||
expect(section).toHaveClass(
|
||||
"py-[var(--spacing-scale-032)]",
|
||||
"lg:py-[var(--spacing-scale-064)]"
|
||||
);
|
||||
});
|
||||
|
||||
it("displays the section heading", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const heading = screen.getByRole("heading", { level: 2 });
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent("Related Articles");
|
||||
expect(heading).toHaveClass(
|
||||
"text-[32px]",
|
||||
"lg:text-[44px]",
|
||||
"leading-[110%]",
|
||||
"font-medium",
|
||||
"text-[var(--color-content-inverse-primary)]",
|
||||
"text-center"
|
||||
);
|
||||
});
|
||||
|
||||
it("renders all related articles", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-3")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("filters out the current post from related articles", () => {
|
||||
const postsWithCurrent = [
|
||||
...mockRelatedPosts,
|
||||
{
|
||||
slug: "current-article",
|
||||
frontmatter: {
|
||||
title: "Current Article",
|
||||
description: "This is the current article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={postsWithCurrent}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
// Should not render the current article
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-current-article")
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Should still render the other related articles
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-3")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders nothing when no related posts", () => {
|
||||
const { container } = render(
|
||||
<RelatedArticles relatedPosts={[]} currentPostSlug="current-article" />
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("renders nothing when all posts are filtered out", () => {
|
||||
const currentPostOnly = [
|
||||
{
|
||||
slug: "current-article",
|
||||
frontmatter: {
|
||||
title: "Current Article",
|
||||
description: "This is the current article",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { container } = render(
|
||||
<RelatedArticles
|
||||
relatedPosts={currentPostOnly}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("has correct container styling", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const container = document.querySelector("section > div");
|
||||
expect(container).toHaveClass(
|
||||
"flex",
|
||||
"flex-col",
|
||||
"gap-[var(--spacing-scale-032)]",
|
||||
"lg:gap-[51px]"
|
||||
);
|
||||
});
|
||||
|
||||
it("has correct articles container styling", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const articlesContainer = document.querySelector("section > div > div");
|
||||
expect(articlesContainer).toHaveClass(
|
||||
"flex",
|
||||
"justify-center",
|
||||
"overflow-hidden"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct responsive behavior for desktop", () => {
|
||||
// Set desktop width
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 1024,
|
||||
});
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const carouselContainer = document.querySelector(
|
||||
"section > div > div > div"
|
||||
);
|
||||
expect(carouselContainer).toHaveClass(
|
||||
"overflow-x-auto",
|
||||
"scrollbar-hide",
|
||||
"cursor-grab",
|
||||
"active:cursor-grabbing"
|
||||
);
|
||||
});
|
||||
|
||||
it("applies correct responsive behavior for mobile", () => {
|
||||
// Set mobile width
|
||||
Object.defineProperty(window, "innerWidth", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 768,
|
||||
});
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const carouselContainer = document.querySelector(
|
||||
"section > div > div > div"
|
||||
);
|
||||
expect(carouselContainer).toHaveClass(
|
||||
"transition-transform",
|
||||
"duration-500",
|
||||
"ease-in-out"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles single related article", () => {
|
||||
const singlePost = [mockRelatedPosts[0]];
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={singlePost}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-related-article-2")
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-related-article-3")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles two related articles", () => {
|
||||
const twoPosts = mockRelatedPosts.slice(0, 2);
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={twoPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("thumbnail-related-article-3")
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has proper accessibility attributes", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const section = document.querySelector("section");
|
||||
expect(section).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies correct gap between articles", () => {
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={mockRelatedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
const carouselContainer = document.querySelector(
|
||||
"section > div > div > div"
|
||||
);
|
||||
expect(carouselContainer).toHaveClass("gap-0");
|
||||
});
|
||||
|
||||
it("handles missing currentPostSlug gracefully", () => {
|
||||
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
|
||||
|
||||
// Should still render all articles
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-1")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-2")
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("thumbnail-related-article-3")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles malformed post data gracefully", () => {
|
||||
const malformedPosts = [
|
||||
{
|
||||
slug: "malformed-1",
|
||||
frontmatter: {
|
||||
title: "Malformed Post 1",
|
||||
description: "Test description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "malformed-2",
|
||||
frontmatter: {
|
||||
title: "Malformed Post 2",
|
||||
description: "Test description",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<RelatedArticles
|
||||
relatedPosts={malformedPosts}
|
||||
currentPostSlug="current-article"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("thumbnail-malformed-1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("thumbnail-malformed-2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,9 @@ describe("Content Processing", () => {
|
||||
vi.spyOn(process, "cwd").mockReturnValue("/mock/project/root");
|
||||
|
||||
// Mock path.join to return predictable paths
|
||||
mockPathJoin.mockImplementation((...args) => args.join("/"));
|
||||
if (mockPathJoin && mockPathJoin.mockImplementation) {
|
||||
mockPathJoin.mockImplementation((...args) => args.join("/"));
|
||||
}
|
||||
});
|
||||
|
||||
describe("getBlogPostFiles", () => {
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
getBlogPostFiles,
|
||||
parseBlogPost,
|
||||
getAllBlogPosts,
|
||||
getBlogPostBySlug,
|
||||
getRelatedBlogPosts,
|
||||
getAllTags,
|
||||
getBlogPostsByTag,
|
||||
} from "../../lib/content.js";
|
||||
|
||||
// Mock fs and path modules
|
||||
vi.mock("fs", () => ({
|
||||
readdirSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("path", () => ({
|
||||
join: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Content Processing", () => {
|
||||
let mockReaddirSync, mockReadFileSync, mockPathJoin;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Get references to the mocked functions
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
mockReaddirSync = fs.readdirSync;
|
||||
mockReadFileSync = fs.readFileSync;
|
||||
mockPathJoin = path.join;
|
||||
|
||||
// Mock process.cwd to return a predictable path
|
||||
vi.spyOn(process, "cwd").mockReturnValue("/mock/project/root");
|
||||
|
||||
// Mock path.join to return predictable paths
|
||||
if (mockPathJoin && mockPathJoin.mockImplementation) {
|
||||
mockPathJoin.mockImplementation((...args) => args.join("/"));
|
||||
}
|
||||
});
|
||||
|
||||
describe("getBlogPostFiles", () => {
|
||||
it("should return markdown files from content directory", () => {
|
||||
const mockFiles = ["post1.md", "post2.mdx", "image.png", "post3.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
const result = getBlogPostFiles();
|
||||
expect(result).toEqual(["post1.md", "post2.mdx", "post3.md"]);
|
||||
expect(mockReaddirSync).toHaveBeenCalledWith(
|
||||
"/mock/project/root/content/blog"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle directory read errors gracefully", () => {
|
||||
mockReaddirSync.mockImplementation(() => {
|
||||
throw new Error("Directory not found");
|
||||
});
|
||||
|
||||
const result = getBlogPostFiles();
|
||||
expect(result).toEqual([]);
|
||||
expect(mockReaddirSync).toHaveBeenCalledWith(
|
||||
"/mock/project/root/content/blog"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBlogPost", () => {
|
||||
it("should parse a valid markdown file", () => {
|
||||
const mockContent = `---
|
||||
title: "Test Post"
|
||||
description: "A test description that meets the minimum length requirement"
|
||||
author: "Test Author"
|
||||
date: "2025-04-15"
|
||||
tags: ["test"]
|
||||
related: []
|
||||
---
|
||||
# Test Content
|
||||
This is the content.`;
|
||||
|
||||
mockReadFileSync.mockReturnValue(mockContent);
|
||||
|
||||
const result = parseBlogPost("test-post.md");
|
||||
expect(result).toMatchObject({
|
||||
slug: "test-post",
|
||||
frontmatter: {
|
||||
title: "Test Post",
|
||||
description:
|
||||
"A test description that meets the minimum length requirement",
|
||||
author: "Test Author",
|
||||
date: "2025-04-15",
|
||||
tags: ["test"],
|
||||
related: [],
|
||||
},
|
||||
content: "\n# Test Content\nThis is the content.",
|
||||
filePath: "test-post.md",
|
||||
});
|
||||
expect(mockReadFileSync).toHaveBeenCalledWith(
|
||||
"/mock/project/root/content/blog/test-post.md",
|
||||
"utf8"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null for invalid frontmatter", () => {
|
||||
const mockContent = `---
|
||||
title: "" # Invalid title
|
||||
description: "A test description"
|
||||
author: "Test Author"
|
||||
date: "2025-04-15"
|
||||
---
|
||||
# Test Content`;
|
||||
|
||||
mockReadFileSync.mockReturnValue(mockContent);
|
||||
|
||||
const result = parseBlogPost("invalid-post.md");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle file read errors gracefully", () => {
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error("File not found");
|
||||
});
|
||||
|
||||
const result = parseBlogPost("non-existent-post.md");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllBlogPosts", () => {
|
||||
it("should return all valid blog posts sorted by date", () => {
|
||||
const mockFiles = ["post1.md", "post2.md", "post3.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
// Mock fs.readFileSync for each post
|
||||
mockReadFileSync.mockReturnValueOnce(`---
|
||||
title: "Post 1"
|
||||
description: "Desc 1"
|
||||
author: "Author 1"
|
||||
date: "2025-04-10"
|
||||
---
|
||||
# Content 1`).mockReturnValueOnce(`---
|
||||
title: "Post 2"
|
||||
description: "Desc 2"
|
||||
author: "Author 2"
|
||||
date: "2025-04-20"
|
||||
---
|
||||
# Content 2`).mockReturnValueOnce(`---
|
||||
title: "Post 3"
|
||||
description: "Desc 3"
|
||||
author: "Author 3"
|
||||
date: "2025-04-05"
|
||||
---
|
||||
# Content 3`);
|
||||
|
||||
const result = getAllBlogPosts();
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].slug).toBe("post2"); // Latest date
|
||||
expect(result[1].slug).toBe("post1");
|
||||
expect(result[2].slug).toBe("post3"); // Oldest date
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBlogPostBySlug", () => {
|
||||
it("should return blog post for valid slug", () => {
|
||||
const mockFiles = ["test-post.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
const mockContent = `---
|
||||
title: "Test Post"
|
||||
description: "A test description that meets the minimum length requirement"
|
||||
author: "Test Author"
|
||||
date: "2025-04-15"
|
||||
---
|
||||
# Test Content`;
|
||||
|
||||
mockReadFileSync.mockReturnValue(mockContent);
|
||||
|
||||
const result = getBlogPostBySlug("test-post");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.slug).toBe("test-post");
|
||||
});
|
||||
|
||||
it("should return null for invalid slug", () => {
|
||||
const mockFiles = ["test-post.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
const result = getBlogPostBySlug("invalid-slug");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRelatedBlogPosts", () => {
|
||||
it("should return related posts when slugs are provided", () => {
|
||||
const mockFiles = ["post1.md", "post2.md", "post3.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
// Mock content for all posts
|
||||
mockReadFileSync.mockReturnValueOnce(`---
|
||||
title: "Post 1"
|
||||
description: "Desc 1"
|
||||
author: "Author 1"
|
||||
date: "2025-04-10"
|
||||
related: ["post2"]
|
||||
---
|
||||
# Content 1`).mockReturnValueOnce(`---
|
||||
title: "Post 2"
|
||||
description: "Desc 2"
|
||||
author: "Author 2"
|
||||
date: "2025-04-20"
|
||||
---
|
||||
# Content 2`).mockReturnValueOnce(`---
|
||||
title: "Post 3"
|
||||
description: "Desc 3"
|
||||
author: "Author 3"
|
||||
date: "2025-04-05"
|
||||
---
|
||||
# Content 3`);
|
||||
|
||||
const result = getRelatedBlogPosts("post1", ["post2", "post3"], 2);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].slug).toBe("post2");
|
||||
expect(result[1].slug).toBe("post3");
|
||||
});
|
||||
|
||||
it("should fallback to recent posts when no related slugs provided", () => {
|
||||
const mockFiles = ["post1.md", "post2.md", "post3.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
const mockContent = `---
|
||||
title: "Post 1"
|
||||
description: "Desc 1"
|
||||
author: "Author 1"
|
||||
date: "2025-04-10"
|
||||
---
|
||||
# Content 1`;
|
||||
|
||||
mockReadFileSync.mockReturnValue(mockContent);
|
||||
|
||||
const result = getRelatedBlogPosts("post1", [], 2);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].slug).toBe("post2"); // Should be the most recent after excluding 'post1'
|
||||
expect(result[1].slug).toBe("post3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAllTags", () => {
|
||||
it("should return unique tags from all posts", () => {
|
||||
const mockFiles = ["post1.md", "post2.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
const mockContent1 = `---
|
||||
title: "Post 1"
|
||||
description: "Desc 1"
|
||||
author: "Author 1"
|
||||
date: "2025-04-10"
|
||||
tags: ["tagA", "tagB"]
|
||||
---
|
||||
# Content 1`;
|
||||
const mockContent2 = `---
|
||||
title: "Post 2"
|
||||
description: "Desc 2"
|
||||
author: "Author 2"
|
||||
date: "2025-04-20"
|
||||
tags: ["tagB", "tagC"]
|
||||
---
|
||||
# Content 2`;
|
||||
|
||||
mockReadFileSync
|
||||
.mockReturnValueOnce(mockContent1)
|
||||
.mockReturnValueOnce(mockContent2);
|
||||
|
||||
const result = getAllTags();
|
||||
expect(result).toEqual(expect.arrayContaining(["tagA", "tagB", "tagC"]));
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBlogPostsByTag", () => {
|
||||
it("should return posts with matching tag", () => {
|
||||
const mockFiles = ["post1.md", "post2.md"];
|
||||
mockReaddirSync.mockReturnValue(mockFiles);
|
||||
|
||||
const mockContent1 = `---
|
||||
title: "Post 1"
|
||||
description: "Desc 1"
|
||||
author: "Author 1"
|
||||
date: "2025-04-10"
|
||||
tags: ["tagA", "tagB"]
|
||||
---
|
||||
# Content 1`;
|
||||
const mockContent2 = `---
|
||||
title: "Post 2"
|
||||
description: "Desc 2"
|
||||
author: "Author 2"
|
||||
date: "2025-04-20"
|
||||
tags: ["tagB", "tagC"]
|
||||
---
|
||||
# Content 2`;
|
||||
|
||||
mockReadFileSync
|
||||
.mockReturnValueOnce(mockContent1)
|
||||
.mockReturnValueOnce(mockContent2);
|
||||
|
||||
const result = getBlogPostsByTag("tagA");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].slug).toBe("post1");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user