From a3a62fab911fb41662d1169949919cc6760c3443 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:59:55 -0600 Subject: [PATCH] Implement E2E tests for content page --- tests/e2e/BlogNavigation.e2e.test.jsx | 201 ++++++++++++++++++++ tests/e2e/ContentPageRendering.e2e.test.jsx | 194 +++++++++++++++++++ tests/e2e/LogoNavigation.e2e.test.jsx | 57 ++++++ vitest.config.mjs | 1 + 4 files changed, 453 insertions(+) create mode 100644 tests/e2e/BlogNavigation.e2e.test.jsx create mode 100644 tests/e2e/ContentPageRendering.e2e.test.jsx create mode 100644 tests/e2e/LogoNavigation.e2e.test.jsx diff --git a/tests/e2e/BlogNavigation.e2e.test.jsx b/tests/e2e/BlogNavigation.e2e.test.jsx new file mode 100644 index 0000000..c2aabb6 --- /dev/null +++ b/tests/e2e/BlogNavigation.e2e.test.jsx @@ -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 }) => ( + { + e.preventDefault(); + mockPush(href); + }} + > + {children} + + ), +})); + +// 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(); + + // 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(); + + // 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(); + + // 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(); + + // 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(); + + // 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(); + + // 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( + + ); + + // 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(); + + // 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" + ); + }); + }); +}); diff --git a/tests/e2e/ContentPageRendering.e2e.test.jsx b/tests/e2e/ContentPageRendering.e2e.test.jsx new file mode 100644 index 0000000..168f3e3 --- /dev/null +++ b/tests/e2e/ContentPageRendering.e2e.test.jsx @@ -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: + "

This is the main content of the test article.

It has multiple paragraphs.

", +}; + +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(); + + // 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(); + + // 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(); + + // 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( + + ); + + // 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( + + ); + + // 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(); + + // 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( +
+ + +
+ ); + + // 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( +
+ + +
+ ); + + // 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"); + }); + }); +}); diff --git a/tests/e2e/LogoNavigation.e2e.test.jsx b/tests/e2e/LogoNavigation.e2e.test.jsx new file mode 100644 index 0000000..7e311c1 --- /dev/null +++ b/tests/e2e/LogoNavigation.e2e.test.jsx @@ -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 }) => ( + + {children} + + ), +})); + +// 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(); + + // 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 tag) + expect(logoLink.tagName).toBe("A"); + }); + + it("should have proper accessibility attributes", () => { + render(); + + 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(); + + // 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"); + }); +}); diff --git a/vitest.config.mjs b/vitest.config.mjs index 72715bd..83c0e50 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -16,6 +16,7 @@ export default defineConfig({ "tests/unit/**/*.test.{js,jsx,ts,tsx}", "tests/integration/**/*.test.{js,jsx,ts,tsx}", "tests/accessibility/**/*.test.{js,jsx,ts,tsx}", + "tests/e2e/**/*.test.{js,jsx,ts,tsx}", ], css: true, coverage: {