ab806fbc16
CI Pipeline / test (20) (pull_request) Successful in 6m28s
CI Pipeline / test (18) (pull_request) Successful in 8m20s
CI Pipeline / e2e (firefox) (pull_request) Successful in 3m15s
CI Pipeline / e2e (webkit) (pull_request) Successful in 3m39s
CI Pipeline / e2e (chromium) (pull_request) Successful in 11m5s
CI Pipeline / visual-regression (pull_request) Successful in 6m4s
CI Pipeline / storybook (pull_request) Successful in 2m27s
CI Pipeline / build (pull_request) Successful in 2m29s
CI Pipeline / performance (pull_request) Successful in 4m54s
397 lines
12 KiB
React
397 lines
12 KiB
React
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import React from "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 next/dynamic to return components synchronously in tests
|
|
vi.mock("next/dynamic", () => {
|
|
return {
|
|
default: (importFn, options) => {
|
|
// In tests, resolve the dynamic import immediately and return the component
|
|
let Component = null;
|
|
importFn().then((mod) => {
|
|
Component = mod.default || mod;
|
|
});
|
|
// Return a synchronous wrapper that uses the mocked component
|
|
return (props) => {
|
|
// Use the mocked RelatedArticles component directly
|
|
if (Component) {
|
|
return <Component {...props} />;
|
|
}
|
|
// Fallback: return the loading placeholder if component not ready
|
|
return options?.loading ? options.loading() : null;
|
|
};
|
|
},
|
|
};
|
|
});
|
|
|
|
// 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(async () => {
|
|
// Reset mocks
|
|
vi.clearAllMocks();
|
|
|
|
// Mock the content functions
|
|
const { getBlogPostBySlug, getAllBlogPosts } =
|
|
await import("../../lib/content");
|
|
vi.mocked(getBlogPostBySlug).mockReturnValue(mockPost);
|
|
vi.mocked(getAllBlogPosts).mockReturnValue([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 (it's a div, not main)
|
|
const mainContainer = document.querySelector("div.min-h-screen");
|
|
expect(mainContainer).toBeInTheDocument();
|
|
expect(mainContainer).toHaveClass(
|
|
"min-h-screen",
|
|
"relative",
|
|
"overflow-hidden",
|
|
);
|
|
// Background color is applied via inline style from frontmatter hex
|
|
expect(mainContainer).toHaveStyle({ backgroundColor: expect.any(String) });
|
|
});
|
|
|
|
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);
|
|
|
|
// Wait for dynamically imported RelatedArticles component to load
|
|
await waitFor(() => {
|
|
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.post-body");
|
|
expect(contentDiv).toHaveClass("post-body");
|
|
expect(contentDiv).toHaveClass("-mt-[var(--spacing-scale-048)]");
|
|
expect(contentDiv).toHaveClass(
|
|
"text-[var(--color-content-inverse-primary)]",
|
|
);
|
|
expect(contentDiv).toHaveClass("text-[16px]");
|
|
expect(contentDiv).toHaveClass("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.post-body");
|
|
expect(contentDiv).toHaveClass("sm:text-[18px]");
|
|
expect(contentDiv).toHaveClass("sm:leading-[130%]");
|
|
expect(contentDiv).toHaveClass("lg:text-[24px]");
|
|
expect(contentDiv).toHaveClass("lg:leading-[32px]");
|
|
expect(contentDiv).toHaveClass("xl:text-[32px]");
|
|
expect(contentDiv).toHaveClass("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.post-body");
|
|
expect(contentDiv).toHaveClass("sm:mx-auto");
|
|
expect(contentDiv).toHaveClass("sm:max-w-[390px]");
|
|
expect(contentDiv).toHaveClass("md:max-w-[472px]");
|
|
expect(contentDiv).toHaveClass("lg:max-w-[700px]");
|
|
expect(contentDiv).toHaveClass("xl:max-w-[904px]");
|
|
});
|
|
|
|
it("includes structured data scripts", async () => {
|
|
const BlogPostPageComponent = await BlogPostPage({
|
|
params: { slug: "test-article" },
|
|
});
|
|
render(BlogPostPageComponent);
|
|
|
|
// Check for script elements using querySelector since RTL ignores them
|
|
const scripts = document.querySelectorAll(
|
|
'script[type="application/ld+json"]',
|
|
);
|
|
expect(scripts).toHaveLength(2);
|
|
|
|
// Check that scripts have the correct type and content
|
|
scripts.forEach((script) => {
|
|
expect(script).toHaveAttribute("type", "application/ld+json");
|
|
expect(script.innerHTML).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
it("handles missing post gracefully", async () => {
|
|
const { getBlogPostBySlug } = await import("../../lib/content");
|
|
vi.mocked(getBlogPostBySlug).mockReturnValue(null);
|
|
|
|
// The component should throw an error when post is null
|
|
// This happens because notFound() is called
|
|
await expect(
|
|
BlogPostPage({ params: { slug: "non-existent" } }),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it("filters out current post from related articles", async () => {
|
|
const BlogPostPageComponent = await BlogPostPage({
|
|
params: { slug: "test-article" },
|
|
});
|
|
render(BlogPostPageComponent);
|
|
|
|
// Wait for dynamically imported RelatedArticles component to load
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("related-articles")).toBeInTheDocument();
|
|
});
|
|
|
|
// 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",
|
|
description: "A malformed post for testing",
|
|
author: "Test Author",
|
|
date: "2025-01-15",
|
|
},
|
|
htmlContent: "<p>Content</p>",
|
|
};
|
|
|
|
const { getBlogPostBySlug } = await import("../../lib/content");
|
|
vi.mocked(getBlogPostBySlug).mockReturnValue(malformedPost);
|
|
|
|
const BlogPostPageComponent = await BlogPostPage({
|
|
params: { slug: "malformed" },
|
|
});
|
|
render(BlogPostPageComponent);
|
|
|
|
expect(screen.getByText("Malformed Post")).toBeInTheDocument();
|
|
expect(screen.getByText("Content")).toBeInTheDocument();
|
|
});
|
|
});
|