Implement E2E tests for content page
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import ContentThumbnailTemplate from "../../app/components/ContentThumbnailTemplate";
|
||||||
|
import RelatedArticles from "../../app/components/RelatedArticles";
|
||||||
|
|
||||||
|
// Mock Next.js navigation
|
||||||
|
const mockPush = vi.fn();
|
||||||
|
vi.mock("next/navigation", () => ({
|
||||||
|
useRouter: () => ({ push: mockPush }),
|
||||||
|
notFound: vi.fn(),
|
||||||
|
usePathname: vi.fn(() => "/"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock Next.js Link to trigger navigation
|
||||||
|
vi.mock("next/link", () => ({
|
||||||
|
default: ({ children, href, ...props }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
{...props}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
mockPush(href);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock asset utils
|
||||||
|
vi.mock("../../lib/assetUtils", () => ({
|
||||||
|
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
|
||||||
|
ASSETS: {
|
||||||
|
CONTENT_THUMBNAIL_1: "Content_Thumbnail_1.svg",
|
||||||
|
CONTENT_THUMBNAIL_2: "Content_Thumbnail_2.svg",
|
||||||
|
CONTENT_THUMBNAIL_3: "Content_Thumbnail_3.svg",
|
||||||
|
CONTENT_ICON_1: "Content_Icon_1.svg",
|
||||||
|
CONTENT_ICON_2: "Content_Icon_2.svg",
|
||||||
|
CONTENT_ICON_3: "Content_Icon_3.svg",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockBlogPost = {
|
||||||
|
slug: "resolving-active-conflicts",
|
||||||
|
frontmatter: {
|
||||||
|
title: "Resolving Active Conflicts",
|
||||||
|
description:
|
||||||
|
"Practical steps for resolving conflicts while maintaining trust",
|
||||||
|
author: "Test Author",
|
||||||
|
date: "2025-04-15",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRelatedPosts = [
|
||||||
|
{
|
||||||
|
slug: "operational-security-mutual-aid",
|
||||||
|
frontmatter: {
|
||||||
|
title: "Operational Security for Mutual Aid",
|
||||||
|
description: "Tactics to protect members, secure communication",
|
||||||
|
author: "Test Author",
|
||||||
|
date: "2025-04-14",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "making-decisions-without-hierarchy",
|
||||||
|
frontmatter: {
|
||||||
|
title: "Making Decisions Without Hierarchy",
|
||||||
|
description:
|
||||||
|
"A brief guide to collaborative nonhierarchical decision making",
|
||||||
|
author: "Test Author",
|
||||||
|
date: "2025-04-13",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("Blog Navigation E2E", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ContentThumbnailTemplate Navigation", () => {
|
||||||
|
it("should navigate to blog post when thumbnail is clicked", () => {
|
||||||
|
render(<ContentThumbnailTemplate post={mockBlogPost} />);
|
||||||
|
|
||||||
|
// Find the thumbnail link
|
||||||
|
const thumbnailLink = screen.getByRole("link");
|
||||||
|
expect(thumbnailLink).toBeInTheDocument();
|
||||||
|
expect(thumbnailLink).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/blog/resolving-active-conflicts"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Click the thumbnail
|
||||||
|
fireEvent.click(thumbnailLink);
|
||||||
|
|
||||||
|
// Verify navigation was called
|
||||||
|
expect(mockPush).toHaveBeenCalledWith("/blog/resolving-active-conflicts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should display correct post information", () => {
|
||||||
|
render(<ContentThumbnailTemplate post={mockBlogPost} />);
|
||||||
|
|
||||||
|
// Verify post content is displayed
|
||||||
|
expect(
|
||||||
|
screen.getByText("Resolving Active Conflicts")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
"Practical steps for resolving conflicts while maintaining trust"
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Test Author")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("April 2025")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render with correct variant based on screen size", () => {
|
||||||
|
render(<ContentThumbnailTemplate post={mockBlogPost} />);
|
||||||
|
|
||||||
|
// Verify the thumbnail container exists
|
||||||
|
const thumbnailContainer = screen.getByRole("link").closest("div");
|
||||||
|
expect(thumbnailContainer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RelatedArticles Navigation", () => {
|
||||||
|
it("should display related articles with correct links", () => {
|
||||||
|
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
|
||||||
|
|
||||||
|
// Verify related articles are displayed
|
||||||
|
expect(
|
||||||
|
screen.getByText("Operational Security for Mutual Aid")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Making Decisions Without Hierarchy")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify links are present
|
||||||
|
const relatedLinks = screen.getAllByRole("link");
|
||||||
|
expect(relatedLinks).toHaveLength(2);
|
||||||
|
expect(relatedLinks[0]).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/blog/operational-security-mutual-aid"
|
||||||
|
);
|
||||||
|
expect(relatedLinks[1]).toHaveAttribute(
|
||||||
|
"href",
|
||||||
|
"/blog/making-decisions-without-hierarchy"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should navigate to related article when clicked", () => {
|
||||||
|
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
|
||||||
|
|
||||||
|
// Find and click first related article
|
||||||
|
const firstRelatedLink = screen
|
||||||
|
.getByText("Operational Security for Mutual Aid")
|
||||||
|
.closest("a");
|
||||||
|
expect(firstRelatedLink).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(firstRelatedLink);
|
||||||
|
|
||||||
|
// Verify navigation was called
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/blog/operational-security-mutual-aid"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty related posts gracefully", () => {
|
||||||
|
const { container } = render(<RelatedArticles relatedPosts={[]} />);
|
||||||
|
|
||||||
|
// Should not crash and should render nothing (component returns null)
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Navigation Flow", () => {
|
||||||
|
it("should complete navigation flow: thumbnail → related article", () => {
|
||||||
|
// Render thumbnail
|
||||||
|
const { rerender } = render(
|
||||||
|
<ContentThumbnailTemplate post={mockBlogPost} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Click thumbnail
|
||||||
|
const thumbnailLink = screen.getByRole("link");
|
||||||
|
fireEvent.click(thumbnailLink);
|
||||||
|
expect(mockPush).toHaveBeenCalledWith("/blog/resolving-active-conflicts");
|
||||||
|
|
||||||
|
// Clear mocks and render related articles
|
||||||
|
vi.clearAllMocks();
|
||||||
|
rerender(<RelatedArticles relatedPosts={mockRelatedPosts} />);
|
||||||
|
|
||||||
|
// Click related article
|
||||||
|
const relatedLink = screen
|
||||||
|
.getByText("Operational Security for Mutual Aid")
|
||||||
|
.closest("a");
|
||||||
|
fireEvent.click(relatedLink);
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
"/blog/operational-security-mutual-aid"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import ContentBanner from "../../app/components/ContentBanner";
|
||||||
|
import AskOrganizer from "../../app/components/AskOrganizer";
|
||||||
|
|
||||||
|
// Mock Next.js navigation
|
||||||
|
vi.mock("next/navigation", () => ({
|
||||||
|
useRouter: () => ({ push: vi.fn() }),
|
||||||
|
notFound: vi.fn(),
|
||||||
|
usePathname: vi.fn(() => "/blog/test-post"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
CONTENT_SHAPE_1: "Content_Shape_1.svg",
|
||||||
|
CONTENT_SHAPE_2: "Content_Shape_2.svg",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockBlogPost = {
|
||||||
|
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 main content of the test article.</p><p>It has multiple paragraphs.</p>",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRelatedPosts = [
|
||||||
|
{
|
||||||
|
slug: "related-article-1",
|
||||||
|
frontmatter: {
|
||||||
|
title: "Related Article 1",
|
||||||
|
description: "First related article",
|
||||||
|
author: "Test Author",
|
||||||
|
date: "2025-04-14",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "related-article-2",
|
||||||
|
frontmatter: {
|
||||||
|
title: "Related Article 2",
|
||||||
|
description: "Second related article",
|
||||||
|
author: "Test Author",
|
||||||
|
date: "2025-04-13",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("Content Page Rendering E2E", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ContentBanner Component", () => {
|
||||||
|
it("should render blog post banner with correct information", () => {
|
||||||
|
render(<ContentBanner post={mockBlogPost} />);
|
||||||
|
|
||||||
|
// Verify banner content
|
||||||
|
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("This is a test article description")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Test Author")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("April 2025")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render with proper semantic structure", () => {
|
||||||
|
render(<ContentBanner post={mockBlogPost} />);
|
||||||
|
|
||||||
|
// Verify semantic HTML structure - ContentBanner doesn't have role="banner"
|
||||||
|
const container = screen.getByText("Test Article Title").closest("div");
|
||||||
|
expect(container).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify headings hierarchy
|
||||||
|
const h3 = screen.getByRole("heading", { level: 3 });
|
||||||
|
expect(h3).toHaveTextContent("Test Article Title");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle different blog posts with different content", () => {
|
||||||
|
const differentPost = {
|
||||||
|
...mockBlogPost,
|
||||||
|
frontmatter: {
|
||||||
|
...mockBlogPost.frontmatter,
|
||||||
|
title: "Different Article Title",
|
||||||
|
description: "Different description",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<ContentBanner post={differentPost} />);
|
||||||
|
|
||||||
|
// Verify different content is rendered
|
||||||
|
expect(screen.getByText("Different Article Title")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Different description")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify old content is not present
|
||||||
|
expect(screen.queryByText("Test Article Title")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("AskOrganizer Component", () => {
|
||||||
|
it("should render ask organizer with correct content", () => {
|
||||||
|
render(
|
||||||
|
<AskOrganizer
|
||||||
|
title="Still have questions?"
|
||||||
|
subtitle="Get help from our community organizers"
|
||||||
|
description="We're here to help you with any questions or concerns."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify ask organizer content
|
||||||
|
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Get help from our community organizers")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: /ask an organizer/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render with inverse variant", () => {
|
||||||
|
render(
|
||||||
|
<AskOrganizer
|
||||||
|
variant="inverse"
|
||||||
|
title="Still have questions?"
|
||||||
|
subtitle="Get help from our community organizers"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify ask organizer content is still present
|
||||||
|
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Get help from our community organizers")
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: /ask an organizer/i })
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have proper accessibility attributes", () => {
|
||||||
|
render(<AskOrganizer />);
|
||||||
|
|
||||||
|
// Verify link is accessible (Button component renders as a link)
|
||||||
|
const link = screen.getByRole("link", { name: /ask an organizer/i });
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
expect(link).toHaveAttribute("href", "#");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Component Integration", () => {
|
||||||
|
it("should render multiple components together", () => {
|
||||||
|
render(
|
||||||
|
<div>
|
||||||
|
<ContentBanner post={mockBlogPost} />
|
||||||
|
<AskOrganizer
|
||||||
|
title="Still have questions?"
|
||||||
|
subtitle="Get help from our community organizers"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify both components are rendered
|
||||||
|
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should maintain proper semantic structure when combined", () => {
|
||||||
|
render(
|
||||||
|
<main>
|
||||||
|
<ContentBanner post={mockBlogPost} />
|
||||||
|
<AskOrganizer
|
||||||
|
title="Still have questions?"
|
||||||
|
subtitle="Get help from our community organizers"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify semantic structure
|
||||||
|
expect(screen.getByRole("main")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("region")).toBeInTheDocument(); // AskOrganizer has role="region"
|
||||||
|
|
||||||
|
// Verify headings hierarchy
|
||||||
|
const h3 = screen.getByRole("heading", { level: 3 });
|
||||||
|
expect(h3).toHaveTextContent("Test Article Title");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import Logo from "../../app/components/Logo";
|
||||||
|
|
||||||
|
// Mock Next.js Link component
|
||||||
|
vi.mock("next/link", () => ({
|
||||||
|
default: ({ children, href, ...props }) => (
|
||||||
|
<a href={href} {...props}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock asset utils
|
||||||
|
vi.mock("../../lib/assetUtils", () => ({
|
||||||
|
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
|
||||||
|
ASSETS: {
|
||||||
|
LOGO: "CommunityRule_Logo.svg",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Logo Navigation E2E", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should navigate to homepage when logo is clicked", () => {
|
||||||
|
render(<Logo />);
|
||||||
|
|
||||||
|
// Find the logo link
|
||||||
|
const logoLink = screen.getByRole("link", { name: /communityrule logo/i });
|
||||||
|
expect(logoLink).toBeInTheDocument();
|
||||||
|
expect(logoLink).toHaveAttribute("href", "/");
|
||||||
|
|
||||||
|
// Verify the link is clickable (Next.js Link renders as <a> tag)
|
||||||
|
expect(logoLink.tagName).toBe("A");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have proper accessibility attributes", () => {
|
||||||
|
render(<Logo />);
|
||||||
|
|
||||||
|
const logoLink = screen.getByRole("link", { name: /communityrule logo/i });
|
||||||
|
expect(logoLink).toHaveAttribute("aria-label", "CommunityRule Logo");
|
||||||
|
expect(logoLink).toHaveAttribute("href", "/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render logo image correctly", () => {
|
||||||
|
render(<Logo />);
|
||||||
|
|
||||||
|
// The image has aria-hidden="true" so we need to find it by alt text
|
||||||
|
const logoImage = screen.getByAltText("CommunityRule Logo Icon");
|
||||||
|
expect(logoImage).toBeInTheDocument();
|
||||||
|
expect(logoImage).toHaveAttribute("src", "/assets/CommunityRule_Logo.svg");
|
||||||
|
expect(logoImage).toHaveAttribute("alt", "CommunityRule Logo Icon");
|
||||||
|
expect(logoImage).toHaveAttribute("aria-hidden", "true");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,6 +16,7 @@ export default defineConfig({
|
|||||||
"tests/unit/**/*.test.{js,jsx,ts,tsx}",
|
"tests/unit/**/*.test.{js,jsx,ts,tsx}",
|
||||||
"tests/integration/**/*.test.{js,jsx,ts,tsx}",
|
"tests/integration/**/*.test.{js,jsx,ts,tsx}",
|
||||||
"tests/accessibility/**/*.test.{js,jsx,ts,tsx}",
|
"tests/accessibility/**/*.test.{js,jsx,ts,tsx}",
|
||||||
|
"tests/e2e/**/*.test.{js,jsx,ts,tsx}",
|
||||||
],
|
],
|
||||||
css: true,
|
css: true,
|
||||||
coverage: {
|
coverage: {
|
||||||
|
|||||||
Reference in New Issue
Block a user