From c8f63ca39a70657a5e08baa05cdda059f8165c44 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:26:21 -0600 Subject: [PATCH] Fix failing tests and add unit tests --- app/blog/[slug]/page.js | 5 +- app/components/Logo.js | 4 +- lib/content.js | 39 +- tests/unit/BlogPage.test.jsx | 360 +++++++++++++++++ tests/unit/ContentBanner.test.jsx | 242 ++++++++++++ tests/unit/ContentContainer.test.jsx | 254 ++++++++++++ tests/unit/ContentThumbnailTemplate.test.jsx | 26 +- tests/unit/Footer.test.jsx | 34 +- tests/unit/Header.test.jsx | 26 +- tests/unit/Logo.test.jsx | 24 +- tests/unit/MarkdownProcessing.test.js | 199 ++++++++++ tests/unit/RelatedArticles.test.jsx | 395 +++++++++++++++++++ tests/unit/content.test.js | 4 +- tests/unit/content.test.js.disabled | 310 +++++++++++++++ 14 files changed, 1833 insertions(+), 89 deletions(-) create mode 100644 tests/unit/BlogPage.test.jsx create mode 100644 tests/unit/ContentBanner.test.jsx create mode 100644 tests/unit/ContentContainer.test.jsx create mode 100644 tests/unit/MarkdownProcessing.test.js create mode 100644 tests/unit/RelatedArticles.test.jsx create mode 100644 tests/unit/content.test.js.disabled diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 2fcb45f..a02c9f5 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -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"; diff --git a/app/components/Logo.js b/app/components/Logo.js index 1d44788..f00b7d3 100644 --- a/app/components/Logo.js +++ b/app/components/Logo.js @@ -116,13 +116,11 @@ export default function Logo({ size = "default", showText = true }) { : sizes.default; return ( - +
{post.frontmatter.description}
+{subtitle}
+ +This is the article content with bold text and italic text.
", +}; + +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: "Content
", + }; + + 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(); + }); +}); diff --git a/tests/unit/ContentBanner.test.jsx b/tests/unit/ContentBanner.test.jsx new file mode 100644 index 0000000..fc34f63 --- /dev/null +++ b/tests/unit/ContentBanner.test.jsx @@ -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 }) => ( + + {children} + + ), + }; +}); + +// 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(This is a paragraph.
"); + }); + + it("converts bold text", () => { + const markdown = "This is **bold** text."; + const result = markdownToHtml(markdown); + + expect(result).toContain("bold"); + }); + + it("converts italic text", () => { + const markdown = "This is *italic* text."; + const result = markdownToHtml(markdown); + + expect(result).toContain("italic"); + }); + + it("converts links", () => { + const markdown = "Visit [Google](https://google.com) for search."; + const result = markdownToHtml(markdown); + + expect(result).toContain('Google'); + }); + + it("converts line breaks toParagraph 1
"); + expect(result).toContain("Paragraph 2
"); + expect(result).toContain("Paragraph 3
"); + }); + + it("adds md-gap class to paragraphs", () => { + const markdown = "Paragraph 1\n\nParagraph 2"; + const result = markdownToHtml(markdown); + + expect(result).toContain('Paragraph 1
'); + expect(result).toContain('Paragraph 2
'); + }); + + it("converts unordered lists", () => { + const markdown = "- Item 1\n- Item 2\n- Item 3"; + const result = markdownToHtml(markdown); + + expect(result).toContain("");
+ expect(result).toContain("");
+ expect(result).toContain("const x = 1;");
+ });
+
+ it("converts inline code", () => {
+ const markdown = "Use `console.log()` to debug.";
+ const result = markdownToHtml(markdown);
+
+ expect(result).toContain("console.log()");
+ });
+
+ it("converts blockquotes", () => {
+ const markdown = "> This is a quote\n> with multiple lines";
+ const result = markdownToHtml(markdown);
+
+ expect(result).toContain("");
+ expect(result).toContain("This is a quote");
+ expect(result).toContain("with multiple lines");
+ expect(result).toContain("
");
+ });
+
+ it("converts horizontal rules", () => {
+ const markdown = "Text above\n\n---\n\nText below";
+ const result = markdownToHtml(markdown);
+
+ expect(result).toContain("
");
+ });
+
+ 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("Title
");
+ expect(result).toContain("bold");
+ expect(result).toContain('link');
+ expect(result).toContain("");
+ expect(result).toContain("- List item 1
");
+ expect(result).toContain("- List item 2
");
+ expect(result).toContain("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("Main Title
");
+ expect(result).toContain("Subtitle
");
+ expect(result).toContain("bold");
+ expect(result).toContain("italic");
+ expect(result).toContain("");
+ expect(result).toContain("code");
+ expect(result).toContain('link');
+ expect(result).toContain("");
+ expect(result).toContain("");
+ });
+
+ 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
");
+ expect(result).toContain("H2
");
+ expect(result).toContain("H3
");
+ expect(result).toContain("H4
");
+ expect(result).toContain("H5
");
+ expect(result).toContain("H6
");
+ });
+
+ it("handles tables", () => {
+ const markdown =
+ "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
+ const result = markdownToHtml(markdown);
+
+ expect(result).toContain("");
+ expect(result).toContain("");
+ expect(result).toContain("Header 1 ");
+ expect(result).toContain("Header 2 ");
+ expect(result).toContain("");
+ expect(result).toContain("Cell 1 ");
+ expect(result).toContain("Cell 2 ");
+ });
+ });
+});
diff --git a/tests/unit/RelatedArticles.test.jsx b/tests/unit/RelatedArticles.test.jsx
new file mode 100644
index 0000000..1fe2951
--- /dev/null
+++ b/tests/unit/RelatedArticles.test.jsx
@@ -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 }) => (
+
+ {children}
+
+ ),
+ };
+});
+
+// Mock ContentThumbnailTemplate
+vi.mock("../../app/components/ContentThumbnailTemplate", () => {
+ return {
+ default: ({ post }) => (
+
+ ),
+ };
+});
+
+// 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ // 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(
+
+ );
+
+ 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(
+
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("has correct container styling", () => {
+ render(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ 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(
+
+ );
+
+ const section = document.querySelector("section");
+ expect(section).toBeInTheDocument();
+ });
+
+ it("applies correct gap between articles", () => {
+ render(
+
+ );
+
+ const carouselContainer = document.querySelector(
+ "section > div > div > div"
+ );
+ expect(carouselContainer).toHaveClass("gap-0");
+ });
+
+ it("handles missing currentPostSlug gracefully", () => {
+ render( );
+
+ // 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(
+
+ );
+
+ expect(screen.getByTestId("thumbnail-malformed-1")).toBeInTheDocument();
+ expect(screen.getByTestId("thumbnail-malformed-2")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/content.test.js b/tests/unit/content.test.js
index 2fff0d2..412f71b 100644
--- a/tests/unit/content.test.js
+++ b/tests/unit/content.test.js
@@ -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", () => {
diff --git a/tests/unit/content.test.js.disabled b/tests/unit/content.test.js.disabled
new file mode 100644
index 0000000..412f71b
--- /dev/null
+++ b/tests/unit/content.test.js.disabled
@@ -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");
+ });
+ });
+});