Update E2E tests and simplify performance tests
CI Pipeline / e2e (chromium) (pull_request) Successful in 6m13s
CI Pipeline / e2e (firefox) (pull_request) Successful in 7m3s
CI Pipeline / e2e (webkit) (pull_request) Successful in 5m52s
CI Pipeline / visual-regression (pull_request) Successful in 7m48s
CI Pipeline / performance (pull_request) Successful in 7m59s
CI Pipeline / lint (pull_request) Successful in 6m16s
CI Pipeline / build (pull_request) Successful in 5m30s
CI Pipeline / test (pull_request) Successful in 6m26s

This commit is contained in:
adilallo
2026-01-28 18:22:59 -07:00
parent 9cb89162ab
commit a30bf6be4c
124 changed files with 452 additions and 4190 deletions
-201
View File
@@ -1,201 +0,0 @@
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",
);
});
});
});
-173
View File
@@ -1,173 +0,0 @@
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>",
};
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");
});
});
});
-57
View File
@@ -1,57 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } 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");
});
});
+217
View File
@@ -0,0 +1,217 @@
import { test, expect } from "@playwright/test";
test.describe("Critical User Journeys", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("complete user journey: learn about CommunityRule", async ({ page }) => {
// 1. User lands on homepage
await expect(page.locator("text=Collaborate")).toBeVisible();
// 2. User reads hero section
await expect(
page.locator("text=Help your community make important decisions"),
).toBeVisible();
// 3. User clicks CTA to learn more
const learnButton = page
.locator('button:has-text("Learn how CommunityRule works")')
.first();
if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) {
await learnButton.click();
}
// 4. User scrolls to numbered cards section
// Note: SectionHeader shows "How CommunityRule works" on mobile, "How CommunityRule helps" on desktop
const howItWorksHeading = page.locator(
'h2:has-text("How CommunityRule works"), h2:has-text("How CommunityRule helps")',
);
await expect(howItWorksHeading).toBeVisible();
// 5. User reads the process steps
await expect(
page.locator("text=Document how your community makes decisions"),
).toBeVisible();
await expect(
page.locator("text=Build an operating manual for a successful community"),
).toBeVisible();
await expect(
page.locator(
"text=Get a link to your manual for your group to review and evolve",
),
).toBeVisible();
// 6. User explores rule templates
await page.locator("text=Consensus clusters").first().click();
await page.locator("text=Consensus").nth(1).click();
await page.locator("text=Elected Board").first().click();
await page.locator("text=Petition").first().click();
// 7. User checks out features
const features = [
"Decision-making support",
"Values alignment exercises",
"Membership guidance",
"Conflict resolution tools",
];
for (const feature of features) {
const featureElement = page.locator(`text=${feature}`);
if (
(await featureElement.count()) > 0 &&
(await featureElement.first().isVisible())
) {
await featureElement.first().click();
}
}
// 8. User reads testimonial
await expect(page.locator("text=Jo Freeman")).toBeVisible();
// 9. User decides to contact organizer
const askButton = page.locator(
'a:has-text("Ask an organizer"), button:has-text("Ask an organizer")',
);
if (
(await askButton.count()) > 0 &&
(await askButton.first().isVisible())
) {
await askButton.first().click();
}
});
test("homepage loads successfully with all sections", async ({ page }) => {
// Check page title
await expect(page).toHaveTitle(/CommunityRule/);
// Check main sections are present
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
const howItWorksHeading = page.locator(
'h2:has-text("How CommunityRule works"), h2:has-text("How CommunityRule helps")',
);
await expect(howItWorksHeading).toBeVisible();
await expect(
page.locator("h1").filter({ hasText: "We've got your back" }),
).toBeVisible();
// Check key components are rendered
await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible();
await expect(
page.locator("text=Trusted by leading cooperators"),
).toBeVisible();
await expect(page.locator("text=Jo Freeman")).toBeVisible();
});
test("feature grid section functionality", async ({ page }) => {
// Check section header
await expect(
page.locator('h1:has-text("We\'ve got your back")'),
).toBeVisible();
await expect(
page.locator(
"text=Use our toolkit to improve, document, and evolve your organization",
),
).toBeVisible();
// Check all four feature cards - FeatureGrid uses section with grid layout
const featureSection = page.locator(
'section[aria-label="Feature tools and services"], section:has-text("We\'ve got your back")',
);
await expect(featureSection.locator("text=Decision-making")).toBeVisible();
await expect(featureSection.locator("text=Values alignment")).toBeVisible();
await expect(featureSection.locator("text=Membership")).toBeVisible();
await expect(
featureSection.locator("text=Conflict resolution"),
).toBeVisible();
// Check feature links - MiniCard components render as <a> tags with href="#..."
// There are 4 feature cards + 1 "Learn more" link = 5 total links
// We check for the specific feature card links
await expect(
featureSection.locator('a[href="#decision-making"]'),
).toBeVisible();
await expect(
featureSection.locator('a[href="#values-alignment"]'),
).toBeVisible();
await expect(
featureSection.locator('a[href="#membership-guidance"]'),
).toBeVisible();
await expect(
featureSection.locator('a[href="#conflict-resolution"]'),
).toBeVisible();
// Test feature card interactions
await page.locator('a[href="#decision-making"]').click();
});
test("header navigation functionality", async ({ page }) => {
// Check header is present
await expect(page.locator("header")).toBeVisible();
// Test logo click
const logoLinks = page.locator('a[aria-label="CommunityRule Logo"]');
const logoCount = await logoLinks.count();
expect(logoCount).toBeGreaterThan(0);
let visibleLogo = null;
for (let i = 0; i < logoCount; i++) {
const logo = logoLinks.nth(i);
if (await logo.isVisible()) {
visibleLogo = logo;
break;
}
}
expect(visibleLogo).not.toBeNull();
await visibleLogo.click();
await expect(page).toHaveURL(/\/#?$/);
});
test("keyboard navigation and accessibility", async ({ page }) => {
// Test tab navigation
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
// Navigate through interactive elements
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
// Test Enter key on buttons
await page.keyboard.press("Enter");
// Basic accessibility checks
const html = page.locator("html");
const lang = await html.getAttribute("lang");
expect(lang).toBeTruthy();
// Check for main heading
const h1 = page.locator("h1").first();
await expect(h1).toBeVisible();
});
test("responsive design behavior", async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
// Test desktop viewport
await page.setViewportSize({ width: 1440, height: 900 });
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
});
});
+19 -538
View File
@@ -6,14 +6,16 @@ test.describe("Edge Cases and Error Scenarios", () => {
});
test("handles slow network conditions", async ({ page }) => {
// Simulate slow network
// Page is already loaded from beforeEach
// Simulate slow network for any subsequent requests
await page.route("**/*", (route) => {
// Add 2 second delay to all requests
setTimeout(() => route.continue(), 2000);
});
// Reload page with slow network
await page.reload();
// Navigate to a new page to test slow network conditions
// Use a fresh navigation instead of reload to avoid Web Inspector issues
await page.goto("/", { waitUntil: "domcontentloaded", timeout: 10000 });
// Page should still load eventually
await expect(page.locator("text=Collaborate")).toBeVisible({
@@ -22,213 +24,23 @@ test.describe("Edge Cases and Error Scenarios", () => {
});
test("handles offline mode gracefully", async ({ page }) => {
// Note: page.setOffline() is not available in current Playwright version
// This test would require network interception to simulate offline mode
// For now, we'll test that the page loads and functions normally
// Page is already loaded from beforeEach, so we can test offline behavior
// without reloading (which is blocked by Web Inspector in local environments)
// Page should function normally
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
});
test("handles rapid user interactions", async ({ page }) => {
// Rapidly click visible buttons
const buttons = page.locator("button");
const buttonCount = await buttons.count();
let clickedCount = 0;
for (let i = 0; i < buttonCount && clickedCount < 3; i++) {
const button = buttons.nth(i);
if (await button.isVisible()) {
await button.click();
await page.waitForTimeout(100); // Very short delay
clickedCount++;
}
}
// Page should remain stable
await expect(page.locator("text=Collaborate")).toBeVisible();
});
test("handles rapid scrolling", async ({ page }) => {
// Rapid scroll to bottom
await page.evaluate(() => {
for (let i = 0; i < 10; i++) {
window.scrollTo(0, document.body.scrollHeight * (i / 10));
}
// Simulate offline mode by blocking all network requests
await page.route("**/*", (route) => {
route.abort();
});
// Should end up at bottom - use a more specific selector
await expect(page.locator("footer").first()).toBeVisible();
});
// Verify page content is still visible (cached content should remain)
// This tests that the page doesn't crash when network requests fail
const body = page.locator("body");
await expect(body).toBeVisible();
test("handles viewport size changes", async ({ page }) => {
// Rapidly change viewport sizes
const viewports = [
{ width: 375, height: 667 },
{ width: 768, height: 1024 },
{ width: 1440, height: 900 },
{ width: 1920, height: 1080 },
];
for (const viewport of viewports) {
await page.setViewportSize(viewport);
await page.waitForTimeout(500);
// Content should remain visible
await expect(page.locator("text=Collaborate")).toBeVisible();
}
});
test("handles browser back/forward navigation", async ({ page }) => {
// Navigate to a section
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
// Since the button click doesn't navigate to a new page,
// we'll test that the page handles back/forward gracefully
await page.goBack();
await page.goForward();
// Should still have content
await expect(page.locator("body")).toBeVisible();
});
test("handles page refresh during interactions", async ({ page }) => {
// Start an interaction
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
// Refresh page during interaction
await page.reload();
// Should reload successfully
// Verify key content is still accessible
await expect(page.locator("text=Collaborate")).toBeVisible();
});
test("handles multiple browser tabs", async ({ page, context }) => {
// Open multiple tabs
const page1 = await context.newPage();
const page2 = await context.newPage();
// Navigate all tabs to homepage
await page1.goto("/");
await page2.goto("/");
// Interact with each tab - find the first visible button
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
await page1.locator("text=Consensus clusters").click();
// Find the first visible "Ask an organizer" link (it's an <a> tag, not a button)
const askLinks = page2.locator('a:has-text("Ask an organizer")');
const askLinkCount = await askLinks.count();
let visibleAskLink = null;
for (let i = 0; i < askLinkCount; i++) {
const link = askLinks.nth(i);
if (await link.isVisible()) {
visibleAskLink = link;
break;
}
}
if (!visibleAskLink) {
throw new Error('No visible "Ask an organizer" link found');
}
await visibleAskLink.click();
// All tabs should work independently
await expect(
page.locator('h2:has-text("How CommunityRule works")'),
).toBeVisible();
await expect(page1.locator("text=Consensus clusters")).toBeVisible();
await expect(page2.locator("text=Still have questions?")).toBeVisible();
// Close extra tabs
await page1.close();
await page2.close();
});
test("handles JavaScript errors gracefully", async ({ page }) => {
// Inject a JavaScript error
await page.evaluate(() => {
@@ -248,6 +60,7 @@ test.describe("Edge Cases and Error Scenarios", () => {
// Page should continue to function
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
@@ -277,345 +90,13 @@ test.describe("Edge Cases and Error Scenarios", () => {
route.abort();
});
// Reload page
await page.reload();
// Navigate to a new page to test missing images
// Use a fresh navigation instead of reload to avoid Web Inspector issues
await page.goto("/", { waitUntil: "domcontentloaded" });
// Page should still function without images
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
});
test("handles CSS loading failures", async ({ page }) => {
// Block CSS requests
await page.route("**/*.css", (route) => {
route.abort();
});
// Reload page
await page.reload();
// Page should still function without styles
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
});
test("handles font loading failures", async ({ page }) => {
// Block font requests
await page.route("**/*.{woff,woff2,ttf,otf}", (route) => {
route.abort();
});
// Reload page
await page.reload();
// Page should still function with fallback fonts
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
});
test("handles memory pressure", async ({ page }) => {
// Simulate memory pressure by creating many elements
await page.evaluate(() => {
// Create temporary elements to simulate memory usage
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div");
div.textContent = `Test element ${i}`;
document.body.appendChild(div);
}
// Clean up
setTimeout(() => {
const testElements = document.querySelectorAll(
'div[textContent*="Test element"]',
);
testElements.forEach((el) => el.remove());
}, 100);
});
// Page should remain functional
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
});
test("handles long content gracefully", async ({ page }) => {
// Add a lot of content to test scrolling performance
await page.evaluate(() => {
const container = document.createElement("div");
container.style.height = "10000px";
container.style.background = "linear-gradient(red, blue)";
document.body.appendChild(container);
});
// Scroll through the content
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// Should handle long content without issues
await expect(page.locator("text=Collaborate")).toBeVisible();
});
test("handles focus management", async ({ page }) => {
// Test focus trapping and management
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
// Navigate through focusable elements
for (let i = 0; i < 10; i++) {
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
}
// Test Shift+Tab for reverse navigation
for (let i = 0; i < 5; i++) {
await page.keyboard.press("Shift+Tab");
await expect(page.locator(":focus")).toBeVisible();
}
});
test("handles keyboard shortcuts", async ({ page }) => {
// Test common keyboard shortcuts
await page.keyboard.press("Home");
await page.keyboard.press("End");
await page.keyboard.press("PageUp");
await page.keyboard.press("PageDown");
// Page should handle shortcuts gracefully
await expect(page.locator("text=Collaborate")).toBeVisible();
});
test("handles copy/paste operations", async ({ page }) => {
// Test text selection and copy
await page.locator("text=Collaborate").selectText();
await page.keyboard.press("Control+c");
// Test paste (should work in input fields if any)
const inputs = page.locator("input, textarea");
if ((await inputs.count()) > 0) {
await inputs.first().click();
await page.keyboard.press("Control+v");
}
});
test("handles right-click context menu", async ({ page }) => {
// Test right-click on various elements
await page.locator("text=Collaborate").click({ button: "right" });
// Find visible button for right-click
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (visibleButton) {
await visibleButton.click({ button: "right" });
}
// Try to right-click on a visible image if it exists
const images = page.locator("img");
const imageCount = await images.count();
let visibleImage = null;
for (let i = 0; i < imageCount; i++) {
const image = images.nth(i);
if (await image.isVisible()) {
visibleImage = image;
break;
}
}
if (visibleImage) {
await visibleImage.click({ button: "right" });
}
// Should handle right-clicks gracefully
await expect(page.locator("text=Collaborate")).toBeVisible();
});
test("handles drag and drop operations", async ({ page }) => {
// Test drag and drop (if applicable)
const draggableElements = page.locator('[draggable="true"]');
const dropZones = page.locator('[data-testid*="drop"], [class*="drop"]');
if (
(await draggableElements.count()) > 0 &&
(await dropZones.count()) > 0
) {
await draggableElements.first().dragTo(dropZones.first());
}
// Page should handle drag operations gracefully
await expect(page.locator("text=Collaborate")).toBeVisible();
});
test("handles print functionality", async ({ page }) => {
// Test print functionality
await page.evaluate(() => {
// Mock print function
window.print = () => {};
});
// Trigger print
await page.keyboard.press("Control+p");
// Should handle print gracefully
await expect(page.locator("text=Collaborate")).toBeVisible();
});
test("handles browser zoom", async ({ page }) => {
// Test different zoom levels
await page.evaluate(() => {
document.body.style.zoom = "0.5";
});
await expect(page.locator("text=Collaborate")).toBeVisible();
await page.evaluate(() => {
document.body.style.zoom = "2.0";
});
await expect(page.locator("text=Collaborate")).toBeVisible();
// Reset zoom
await page.evaluate(() => {
document.body.style.zoom = "1.0";
});
});
test("handles high contrast mode", async ({ page }) => {
// Simulate high contrast mode
await page.evaluate(() => {
document.body.style.filter = "contrast(200%)";
});
// Content should remain readable
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
// Reset contrast
await page.evaluate(() => {
document.body.style.filter = "none";
});
});
test("handles reduced motion preferences", async ({ page }) => {
// Simulate reduced motion preference
await page.evaluate(() => {
document.documentElement.style.setProperty(
"--prefers-reduced-motion",
"reduce",
);
});
// Page should respect reduced motion
await expect(page.locator("text=Collaborate")).toBeVisible();
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
-485
View File
@@ -1,485 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("Homepage", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("homepage loads successfully with all sections", async ({ page }) => {
// Check page title and meta
await expect(page).toHaveTitle(/CommunityRule/);
// Check main sections are present
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
await expect(
page.locator("h2").filter({ hasText: "How CommunityRule works" }),
).toBeVisible();
await expect(
page.locator("h1").filter({ hasText: "We've got your back" }),
).toBeVisible();
// Check key components are rendered
await expect(page.locator('img[alt="Hero illustration"]')).toBeVisible();
await expect(
page.locator("text=Trusted by leading cooperators"),
).toBeVisible();
await expect(page.locator("text=Jo Freeman")).toBeVisible();
});
test("hero banner section functionality", async ({ page }) => {
// Check hero content
await expect(page.locator("text=Collaborate")).toBeVisible();
await expect(page.locator("text=with clarity")).toBeVisible();
await expect(
page.locator("text=Help your community make important decisions"),
).toBeVisible();
// Check CTA button
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await expect(visibleButton).toBeEnabled();
// Test button interaction
await visibleButton.click();
// Should scroll to the numbered cards section
await expect(
page.locator('h2:has-text("How CommunityRule works")'),
).toBeVisible();
});
test("logo wall section displays correctly", async ({ page }) => {
// Check section label
await expect(
page.locator("text=Trusted by leading cooperators"),
).toBeVisible();
// Check logos are present
await expect(page.locator('img[alt="Food Not Bombs"]')).toBeVisible();
await expect(page.locator('img[alt="Start COOP"]')).toBeVisible();
await expect(page.locator('img[alt="Metagov"]')).toBeVisible();
await expect(page.locator('img[alt="Open Civics"]')).toBeVisible();
await expect(page.locator('img[alt="Mutual Aid CO"]')).toBeVisible();
await expect(page.locator('img[alt="CU Boulder"]')).toBeVisible();
// Check logos have proper attributes
const logos = page.locator('img[alt*="Logo"]');
const logoCount = await logos.count();
expect(logoCount).toBeGreaterThan(0);
// Test hover effects (visual test)
await page.locator('img[alt="Food Not Bombs"]').hover();
// Should see hover state (opacity change)
});
test("numbered cards section functionality", async ({ page }) => {
// Check section header
await expect(
page.locator('h2:has-text("How CommunityRule works")'),
).toBeVisible();
await expect(
page.locator("text=Here's a quick overview of the process"),
).toBeVisible();
// Check all three cards are present
await expect(
page.locator("text=Document how your community makes decisions"),
).toBeVisible();
await expect(
page.locator("text=Build an operating manual for a successful community"),
).toBeVisible();
await expect(
page.locator(
"text=Get a link to your manual for your group to review and evolve",
),
).toBeVisible();
// Check numbered indicators - target the specific numbered cards section
const numberedCardsSection = page
.locator("section")
.filter({ has: page.locator('h2:has-text("How CommunityRule works")') });
await expect(
numberedCardsSection.locator("span").filter({ hasText: "1" }).first(),
).toBeVisible();
await expect(
numberedCardsSection.locator("span").filter({ hasText: "2" }).first(),
).toBeVisible();
await expect(
numberedCardsSection.locator("span").filter({ hasText: "3" }).first(),
).toBeVisible();
// Check CTA buttons
const createButtons = page.locator(
'button:has-text("Create CommunityRule")',
);
const createButtonCount = await createButtons.count();
let visibleCreateButton = null;
for (let i = 0; i < createButtonCount; i++) {
const button = createButtons.nth(i);
if (await button.isVisible()) {
visibleCreateButton = button;
break;
}
}
if (visibleCreateButton) {
await expect(visibleCreateButton).toBeVisible();
}
// Check for responsive button visibility
const seeHowItWorksButton = page.locator(
'button:has-text("See how it works")',
);
const createCommunityRuleButton = page.locator(
'button:has-text("Create CommunityRule")',
);
// On mobile, "Create CommunityRule" should be visible, "See how it works" should be hidden
// On desktop, "See how it works" should be visible, "Create CommunityRule" should be hidden
const viewport = page.viewportSize();
if (viewport && viewport.width < 1024) {
// Mobile viewport
await expect(createCommunityRuleButton).toBeVisible();
await expect(seeHowItWorksButton).toBeHidden();
} else {
// Desktop viewport
await expect(seeHowItWorksButton).toBeVisible();
await expect(createCommunityRuleButton).toBeHidden();
}
});
test("rule stack section interactions", async ({ page }) => {
// Check all four rule cards are present
await expect(page.locator("text=Consensus clusters")).toBeVisible();
await expect(page.locator("text=Consensus clusters")).toBeVisible();
await expect(page.locator("text=Elected Board").first()).toBeVisible();
await expect(page.locator("text=Petition")).toBeVisible();
// Check rule descriptions
await expect(
page.locator("text=Units called Circles have the ability to decide"),
).toBeVisible();
await expect(
page.locator("text=Decisions that affect the group collectively"),
).toBeVisible();
await expect(
page.locator("text=An elected board determines policies"),
).toBeVisible();
await expect(
page.locator("text=All participants can propose and vote"),
).toBeVisible();
// Test card interactions
const consensusCard = page.locator('[aria-label*="Consensus clusters"]');
await consensusCard.click();
// Should trigger analytics tracking (console log in test environment)
// Check "See all templates" button
await expect(
page.locator('button:has-text("See all templates")'),
).toBeVisible();
});
test("feature grid section functionality", async ({ page }) => {
// Check section header
await expect(
page.locator('h1:has-text("We\'ve got your back")'),
).toBeVisible();
await expect(
page.locator(
"text=Use our toolkit to improve, document, and evolve your organization",
),
).toBeVisible();
// Check all four feature cards - use more specific selectors to avoid conflicts
const featureGrid = page.locator('[role="grid"]');
await expect(featureGrid.locator("text=Decision-making")).toBeVisible();
await expect(featureGrid.locator("text=Values alignment")).toBeVisible();
await expect(featureGrid.locator("text=Membership")).toBeVisible();
await expect(featureGrid.locator("text=Conflict resolution")).toBeVisible();
// Check feature links - be more specific to only get the feature grid links
const featureLinks = featureGrid.locator('a[href^="#"]');
await expect(featureLinks).toHaveCount(4);
// Test feature card interactions
await page.locator('a[href="#decision-making"]').click();
// Should navigate to decision-making section
});
test("quote block section displays correctly", async ({ page }) => {
// Check quote content
await expect(
page.locator("text=The rules of decision-making must be open"),
).toBeVisible();
// Check author and source
await expect(page.locator("text=Jo Freeman")).toBeVisible();
await expect(
page.locator("text=The Tyranny of Structurelessness"),
).toBeVisible();
// Check avatar
await expect(
page.locator('img[alt="Portrait of Jo Freeman"]'),
).toBeVisible();
// Check decorative elements
await expect(
page.locator('[class*="pointer-events-none absolute z-0"]').first(),
).toBeVisible();
});
test("ask organizer section functionality", async ({ page }) => {
// Check section content
await expect(page.locator("text=Still have questions?")).toBeVisible();
await expect(
page.locator("text=Get answers from an experienced organizer"),
).toBeVisible();
// Check CTA button (it's actually a link)
const askLinks = page.locator('a:has-text("Ask an organizer")');
const askLinkCount = await askLinks.count();
let visibleAskLink = null;
for (let i = 0; i < askLinkCount; i++) {
const link = askLinks.nth(i);
if (await link.isVisible()) {
visibleAskLink = link;
break;
}
}
if (!visibleAskLink) {
throw new Error('No visible "Ask an organizer" link found');
}
await expect(visibleAskLink).toBeEnabled();
// Test link interaction
await visibleAskLink.click();
// Should trigger analytics tracking
});
test("header navigation functionality", async ({ page }) => {
// Check header is present
await expect(page.locator("header")).toBeVisible();
// Check navigation elements
await expect(page.locator("nav").first()).toBeVisible();
// Test logo click specifically (not the entire header)
// The logo has different visibility classes for different breakpoints
// Find any visible logo link
const logoLinks = page.locator('a[aria-label="CommunityRule Logo"]');
const logoCount = await logoLinks.count();
expect(logoCount).toBeGreaterThan(0);
// Find the first visible logo link
let visibleLogo = null;
for (let i = 0; i < logoCount; i++) {
const logo = logoLinks.nth(i);
if (await logo.isVisible()) {
visibleLogo = logo;
break;
}
}
expect(visibleLogo).not.toBeNull();
await visibleLogo.click();
// Should navigate to homepage
await expect(page).toHaveURL(/\/#?$/);
});
test("footer section displays correctly", async ({ page }) => {
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// Check footer is present - use the main page footer, not the quote footer
const mainFooter = page.locator("footer").last();
await expect(mainFooter).toBeVisible();
// Check footer content
await expect(mainFooter).toContainText("CommunityRule");
});
test("responsive design behavior", async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
// Test desktop viewport
await page.setViewportSize({ width: 1440, height: 900 });
await expect(
page.locator("h1, h2").filter({ hasText: "Collaborate" }),
).toBeVisible();
});
test("keyboard navigation and accessibility", async ({ page }) => {
// Test tab navigation
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
// Navigate through interactive elements
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
// Test Enter key on buttons
await page.keyboard.press("Enter");
// Test Escape key
await page.keyboard.press("Escape");
});
test("page performance metrics", async ({ page }) => {
// Measure page load time
const startTime = Date.now();
await page.goto("/");
const loadTime = Date.now() - startTime;
// Page should load within reasonable time (5 seconds)
expect(loadTime).toBeLessThan(5000);
// Check for any console errors
const consoleErrors: string[] = [];
page.on("console", (msg) => {
if (msg.type() === "error") {
consoleErrors.push(msg.text());
}
});
await page.reload();
expect(consoleErrors.length).toBe(0);
});
test("accessibility standards compliance", async ({ page }) => {
// Basic accessibility checks
const html = page.locator("html");
const lang = await html.getAttribute("lang");
expect(lang).toBeTruthy();
// Check for main heading
const h1 = page.locator("h1").first();
await expect(h1).toBeVisible();
// Check for main landmark
const main = page.locator("main, [role='main']");
await expect(main).toBeVisible();
// Check for navigation
const nav = page.locator("nav, [role='navigation']").first();
await expect(nav).toBeVisible();
});
test("scroll behavior and smooth scrolling", async ({ page }) => {
// Test smooth scrolling to sections
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
throw new Error(
'No visible "Learn how CommunityRule works" button found',
);
}
await visibleButton.click();
// Should smoothly scroll to numbered cards section
await page.waitForTimeout(1000); // Wait for scroll animation
// Check we're at the numbered cards section
await expect(
page.locator('h2:has-text("How CommunityRule works")'),
).toBeVisible();
});
test("image loading and optimization", async ({ page }) => {
// Check all images load properly
const images = page.locator("img");
const imageCount = await images.count();
expect(imageCount).toBeGreaterThan(0);
// Wait for page to be stable, but don't wait indefinitely for images
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(2000); // Give images time to load
// Check for any broken images, but be more lenient
const brokenImages = await page.evaluate(() => {
const imgs = document.querySelectorAll("img");
return Array.from(imgs).filter(
(img) => !img.complete || img.naturalWidth === 0,
);
});
// Allow some images to be loading (not necessarily broken)
// Only fail if more than 50% of images are broken
const brokenRatio = brokenImages.length / imageCount;
expect(brokenRatio).toBeLessThan(0.5);
});
test("form interactions and validation", async ({ page }) => {
// Test any form elements (if present)
const forms = page.locator("form");
const formCount = await forms.count();
if (formCount > 0) {
// Test form submission
const submitButton = page.locator('button[type="submit"]');
if ((await submitButton.count()) > 0) {
await submitButton.click();
// Should handle form submission appropriately
}
}
});
test("error handling and fallbacks", async ({ page }) => {
// Test with slow network
await page.route("**/*", (route) => {
route.continue();
});
// Test with offline mode (page.setOffline() not available in current Playwright)
// Instead, test that the page loads and functions normally
await expect(page.locator("body")).toBeVisible();
});
});
+42 -359
View File
@@ -1,89 +1,66 @@
import { test, expect } from "@playwright/test";
import { PlaywrightPerformanceMonitor } from "../performance/performance-monitor.js";
// Environment-aware performance budgets and thresholds
// Adjusted for development environment
// Performance budgets - simplified for E2E tests
// Comprehensive performance testing is handled by Lighthouse CI
const PERFORMANCE_BUDGETS = {
// Page load performance
page_load_time: 4000, // 4 seconds - increased for dev environment
first_contentful_paint: 2500, // 2.5 seconds - increased for dev environment
largest_contentful_paint: 3000, // 3 seconds - increased for dev environment
first_input_delay: 150, // 150ms - increased for dev environment
// Navigation timing
dns_lookup: 100, // 100ms
tcp_connection: 200, // 200ms
ttfb: 1500, // 1500ms - increased to be more realistic for development environment and mobile
dom_content_loaded: 2000, // 2 seconds - increased for dev environment
full_load: 4000, // 4 seconds - increased for dev environment
// Component performance
component_render_time: 700, // 700ms - increased for dev environment
interaction_time: 1000, // 1000ms - increased for development environment and mobile
scroll_performance: process.env.CI ? 250 : 150, // More realistic for dev and mobile (150ms vs 100ms)
// Resource performance
network_request_duration: 1500, // 1.5 seconds - increased for dev environment
memory_usage_mb: 60, // 60MB - increased for dev environment
};
// Baseline metrics for regression detection
// Adjusted for development environment (more realistic baselines)
const BASELINE_METRICS = {
page_load_time: 2500, // Increased from 2000ms
first_contentful_paint: 1800, // Increased from 1500ms
largest_contentful_paint: 2200, // Increased from 2000ms
first_input_delay: 80, // Increased from 50ms
dns_lookup: 50,
tcp_connection: 100,
ttfb: 600, // Increased from 400ms to be more realistic for dev
dom_content_loaded: 1200, // Increased from 1000ms
full_load: 2500, // Increased from 2000ms
component_render_time: 400, // Increased from 300ms
interaction_time: 200, // Increased from 100ms to be more realistic for mobile
scroll_performance: 100, // Increased from 60ms to be more realistic for mobile
network_request_duration: 700, // Increased from 500ms
memory_usage_mb: 40, // Increased from 30MB
page_load_time: 4000, // 4 seconds
first_contentful_paint: 2500, // 2.5 seconds
largest_contentful_paint: 3000, // 3 seconds
first_input_delay: 150, // 150ms
ttfb: 1500, // 1.5 seconds
dom_content_loaded: 2000, // 2 seconds
full_load: 4000, // 4 seconds
};
test.describe("Performance Monitoring", () => {
let performanceMonitor: PlaywrightPerformanceMonitor;
test.beforeEach(async ({ page }) => {
// Mark tests as slower in CI environment
test.beforeEach(async () => {
if (process.env.CI) test.slow();
performanceMonitor = new PlaywrightPerformanceMonitor(page);
performanceMonitor.setThresholds(PERFORMANCE_BUDGETS);
performanceMonitor.setBaselines(BASELINE_METRICS);
});
test("homepage load performance", async ({ page: _page }) => {
const result = await performanceMonitor.measurePageLoad("/");
test("homepage load performance", async ({ page }) => {
const startTime = Date.now();
// Navigate to homepage
await page.goto("/", { waitUntil: "load", timeout: 60000 });
const loadTime = Date.now() - startTime;
// Get performance metrics from browser
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType("navigation")[0];
const paint = performance.getEntriesByType("paint");
return {
ttfb: navigation?.responseStart - navigation?.requestStart || 0,
domContentLoaded:
navigation?.domContentLoadedEventEnd -
navigation?.domContentLoadedEventStart || 0,
load: navigation?.loadEventEnd - navigation?.loadEventStart || 0,
firstContentfulPaint:
paint.find((p) => p.name === "first-contentful-paint")?.startTime ||
0,
};
});
// Assert page load time is within budget
expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time);
expect(loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time);
// Assert individual metrics
expect(result.metrics.ttfb).toBeLessThan(PERFORMANCE_BUDGETS.ttfb);
expect(result.metrics.domContentLoaded).toBeLessThan(
expect(metrics.ttfb).toBeLessThan(PERFORMANCE_BUDGETS.ttfb);
expect(metrics.domContentLoaded).toBeLessThan(
PERFORMANCE_BUDGETS.dom_content_loaded,
);
expect(result.metrics.load).toBeLessThan(PERFORMANCE_BUDGETS.full_load);
// Check for performance regressions
const summary = performanceMonitor.getSummary();
console.log("Performance Summary:", summary);
expect(metrics.load).toBeLessThan(PERFORMANCE_BUDGETS.full_load);
expect(metrics.firstContentfulPaint).toBeLessThan(
PERFORMANCE_BUDGETS.first_contentful_paint,
);
});
test("core web vitals", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Wait for page to fully load
// Use "load" state instead of "networkidle" to handle dynamically imported components
await page.waitForLoadState("load");
// Get Core Web Vitals with timeout
// Get Core Web Vitals using browser Performance API
const coreWebVitals = (await page.evaluate(() => {
return new Promise<{ lcp: number; fid: number; cls: number }>(
(resolve) => {
@@ -145,298 +122,4 @@ test.describe("Performance Monitoring", () => {
);
expect(coreWebVitals.cls).toBeLessThan(0.1); // CLS should be less than 0.1
});
test("component render performance", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Measure header render time
const headerRenderTime =
await performanceMonitor.measureComponentRender("header");
expect(headerRenderTime).toBeLessThan(
PERFORMANCE_BUDGETS.component_render_time,
);
// Measure footer render time
const footerRenderTime =
await performanceMonitor.measureComponentRender("footer");
expect(footerRenderTime).toBeLessThan(
PERFORMANCE_BUDGETS.component_render_time,
);
// Measure main content render time
const mainRenderTime =
await performanceMonitor.measureComponentRender("main");
expect(mainRenderTime).toBeLessThan(
PERFORMANCE_BUDGETS.component_render_time,
);
});
test("interaction performance", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Wait for page to be ready
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(1000); // Give page time to stabilize
// Measure button click performance with better element selection
const buttonClickTime = await performanceMonitor.measureInteraction(
'button:has-text("Learn how CommunityRule works")',
async () => {
const learnButtons = page.locator(
'button:has-text("Learn how CommunityRule works")',
);
const buttonCount = await learnButtons.count();
let visibleButton = null;
for (let i = 0; i < buttonCount; i++) {
const button = learnButtons.nth(i);
if (await button.isVisible()) {
visibleButton = button;
break;
}
}
if (!visibleButton) {
// Skip this test if button is not visible (might be hidden on some viewports)
console.log("Button not visible, skipping button click test");
return;
}
await visibleButton.click();
},
);
if (buttonClickTime !== null) {
expect(buttonClickTime).toBeLessThan(
PERFORMANCE_BUDGETS.interaction_time,
);
}
// Measure link click performance with better element selection
const linkClickTime = await performanceMonitor.measureInteraction(
'a:has-text("Use cases")',
async () => {
const useCaseLinks = page.locator('a:has-text("Use cases")');
const linkCount = await useCaseLinks.count();
let visibleLink = null;
for (let i = 0; i < linkCount; i++) {
const link = useCaseLinks.nth(i);
if (await link.isVisible()) {
visibleLink = link;
break;
}
}
if (!visibleLink) {
// Skip this test if link is not visible
console.log("Link not visible, skipping link click test");
return;
}
await visibleLink.click();
},
);
if (linkClickTime !== null) {
expect(linkClickTime).toBeLessThan(PERFORMANCE_BUDGETS.interaction_time);
}
});
test("scroll performance", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Measure scroll performance
const scrollTime = await performanceMonitor.measureScrollPerformance();
expect(scrollTime).toBeLessThan(PERFORMANCE_BUDGETS.scroll_performance);
});
test("memory usage", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Get memory usage
const memoryUsage = await performanceMonitor.getMemoryUsage();
if (memoryUsage) {
const usedMemoryMB = memoryUsage.usedJSHeapSize / 1024 / 1024;
expect(usedMemoryMB).toBeLessThan(PERFORMANCE_BUDGETS.memory_usage_mb);
console.log(`Memory Usage: ${usedMemoryMB.toFixed(2)}MB`);
}
});
test("network request performance", async ({ page }) => {
await performanceMonitor.monitorNetworkRequests();
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Wait for load state instead of networkidle to handle dynamic imports
await page.waitForLoadState("load");
// Check that all requests completed within budget
const summary = performanceMonitor.getSummary();
if (summary.network_request_duration) {
expect(summary.network_request_duration.average).toBeLessThan(
PERFORMANCE_BUDGETS.network_request_duration,
);
}
});
test("responsive performance across breakpoints", async ({ page }) => {
const breakpoints = [
{ name: "mobile", width: 375, height: 667 },
{ name: "tablet", width: 768, height: 1024 },
{ name: "desktop", width: 1280, height: 720 },
];
for (const breakpoint of breakpoints) {
await page.setViewportSize(breakpoint);
const result = await performanceMonitor.measurePageLoad("/");
// Assert performance is maintained across breakpoints
expect(result.loadTime).toBeLessThan(PERFORMANCE_BUDGETS.page_load_time);
console.log(`${breakpoint.name} load time: ${result.loadTime}ms`);
}
});
test("performance under load", async ({ page }) => {
// Simulate slower network conditions
await page.route("**/*", (route) => {
route.continue();
});
// Add artificial delay to simulate network latency
await page.addInitScript(() => {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms delay
return originalFetch(...args);
};
});
const result = await performanceMonitor.measurePageLoad("/");
// Even under load, page should load within reasonable time
expect(result.loadTime).toBeLessThan(
PERFORMANCE_BUDGETS.page_load_time * 1.5,
);
});
test("performance regression detection", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Simulate a performance regression by adding a heavy operation
await page.addInitScript(() => {
// Add a heavy operation that would cause regression
const heavyOperation = () => {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
};
// Execute heavy operation on page load
window.addEventListener("load", () => {
heavyOperation();
});
});
await performanceMonitor.measurePageLoad("/");
// This should trigger a performance regression warning
const summary = performanceMonitor.getSummary();
console.log("Performance Summary with Regression:", summary);
});
test("performance metrics export", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Perform various operations to collect metrics
await performanceMonitor.measureComponentRender("header");
await performanceMonitor.measureScrollPerformance();
await performanceMonitor.getMemoryUsage();
// Export all metrics
const exportedData = performanceMonitor.export();
// Verify exported data structure
expect(exportedData.metrics).toBeDefined();
expect(exportedData.baselines).toBeDefined();
expect(exportedData.thresholds).toBeDefined();
expect(exportedData.summary).toBeDefined();
console.log(
"Exported Performance Data:",
JSON.stringify(exportedData, null, 2),
);
});
test("performance budget compliance", async ({ page }) => {
await page.goto("/", { waitUntil: "load", timeout: 60000 });
// Collect comprehensive metrics
await performanceMonitor.measurePageLoad("/");
await performanceMonitor.measureComponentRender("header");
await performanceMonitor.measureComponentRender("footer");
await performanceMonitor.measureScrollPerformance();
await performanceMonitor.getMemoryUsage();
const summary = performanceMonitor.getSummary();
// Check all metrics against budgets
for (const [metricName, budget] of Object.entries(PERFORMANCE_BUDGETS)) {
if (summary[metricName]) {
const actualValue =
summary[metricName].latest || summary[metricName].average;
expect(actualValue).toBeLessThan(budget);
console.log(`${metricName}: ${actualValue}ms (budget: ${budget}ms)`);
}
}
});
});
test.describe("Performance Regression Testing", () => {
test("detect performance regressions over time", async ({ page }) => {
const performanceMonitor = new PlaywrightPerformanceMonitor(page);
// Set strict baselines for regression detection
const strictBaselines = {
page_load_time: 1500,
first_contentful_paint: 1000,
component_render_time: 200,
interaction_time: 30,
};
performanceMonitor.setBaselines(strictBaselines);
// Run multiple iterations to detect trends
const iterations = 3;
const results = [];
for (let i = 0; i < iterations; i++) {
// measurePageLoad already handles timeouts and wait conditions
const result = await performanceMonitor.measurePageLoad("/");
results.push(result.loadTime);
// Small delay between iterations
await page.waitForTimeout(1000);
}
// Check for consistent performance
const averageLoadTime = results.reduce((a, b) => a + b, 0) / results.length;
const variance =
results.reduce(
(acc, val) => acc + Math.pow(val - averageLoadTime, 2),
0,
) / results.length;
// Performance should be consistent (low variance)
// Increased threshold for development environment which has more variability
expect(variance).toBeLessThan(600000); // Variance should be less than 600ms² for dev environment
console.log(`Average load time: ${averageLoadTime}ms`);
console.log(`Variance: ${variance}`);
});
});
-396
View File
@@ -1,396 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("User Journeys", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("complete user journey: learn about CommunityRule", async ({ page }) => {
// 1. User lands on homepage
await expect(page.locator("text=Collaborate")).toBeVisible();
// 2. User reads hero section
await expect(
page.locator("text=Help your community make important decisions"),
).toBeVisible();
// 3. User clicks CTA to learn more
const learnButton = page
.locator('button:has-text("Learn how CommunityRule works")')
.first();
if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) {
await learnButton.click();
}
// 4. User scrolls to numbered cards section
await expect(
page.locator('h2:has-text("How CommunityRule works")'),
).toBeVisible();
// 5. User reads the process steps
await expect(
page.locator("text=Document how your community makes decisions"),
).toBeVisible();
await expect(
page.locator("text=Build an operating manual for a successful community"),
).toBeVisible();
await expect(
page.locator(
"text=Get a link to your manual for your group to review and evolve",
),
).toBeVisible();
// 6. User explores rule templates
await page.locator("text=Consensus clusters").first().click();
await page.locator("text=Consensus").nth(1).click(); // Use nth(1) to get the second "Consensus" element
await page.locator("text=Elected Board").first().click();
await page.locator("text=Petition").first().click();
// 7. User checks out features - check if elements exist and are visible first
const features = [
"Decision-making support",
"Values alignment exercises",
"Membership guidance",
"Conflict resolution tools",
];
for (const feature of features) {
const featureElement = page.locator(`text=${feature}`);
if (
(await featureElement.count()) > 0 &&
(await featureElement.first().isVisible())
) {
await featureElement.first().click();
}
}
// 8. User reads testimonial
await expect(page.locator("text=Jo Freeman")).toBeVisible();
// 9. User decides to contact organizer
const askButton = page.locator('button:has-text("Ask an organizer")');
if (
(await askButton.count()) > 0 &&
(await askButton.first().isVisible())
) {
await askButton.first().click();
}
// 10. User creates CommunityRule
const createButton = page.locator(
'button:has-text("Create CommunityRule")',
);
if (
(await createButton.count()) > 0 &&
(await createButton.first().isVisible())
) {
await createButton.first().click();
}
});
test("user journey: explore rule templates", async ({ page }) => {
// Scroll to rule stack section
await page.locator("text=Consensus clusters").scrollIntoViewIfNeeded();
// Explore each rule template
const ruleTemplates = [
"Consensus clusters",
"Consensus",
"Elected Board",
"Petition",
];
for (const template of ruleTemplates) {
const templateElement = page.locator(`text=${template}`);
if (template === "Consensus") {
await templateElement.nth(1).click(); // Use nth(1) for the second "Consensus" element
} else {
await templateElement.first().click();
}
// Should trigger analytics tracking
await page.waitForTimeout(500); // Brief pause between clicks
}
// Click "See all templates"
await page.locator('button:has-text("See all templates")').click();
});
test("user journey: explore feature tools", async ({ page }) => {
// Scroll to feature grid section
await page.locator("text=We've got your back").scrollIntoViewIfNeeded();
// Explore each feature
const features = [
{ name: "Decision-making support", href: "#decision-making" },
{ name: "Values alignment exercises", href: "#values-alignment" },
{ name: "Membership guidance", href: "#membership-guidance" },
{ name: "Conflict resolution tools", href: "#conflict-resolution" },
];
for (const feature of features) {
await page.locator(`a[href="${feature.href}"]`).click();
await page.waitForTimeout(500);
}
});
test("user journey: contact organizer", async ({ page }) => {
// Scroll to ask organizer section
await page.locator("text=Still have questions?").scrollIntoViewIfNeeded();
// Read the section
await expect(
page.locator("text=Get answers from an experienced organizer"),
).toBeVisible();
// Click contact button - check if it exists and is visible first
const askButton = page.locator('button:has-text("Ask an organizer")');
if (
(await askButton.count()) > 0 &&
(await askButton.first().isVisible())
) {
await askButton.first().click();
}
// Should trigger analytics tracking
// In a real app, this might open a contact form or modal
});
test("user journey: create CommunityRule", async ({ page }) => {
// Simplified approach - just check if the button exists and is visible
const createButton = page.locator(
'button:has-text("Create CommunityRule")',
);
if (
(await createButton.count()) > 0 &&
(await createButton.first().isVisible())
) {
await createButton.first().click();
}
// Should navigate to creation flow
// In a real app, this would go to a form or wizard
});
test("user journey: learn how it works", async ({ page }) => {
// This test simulates a user learning about how CommunityRule works
// Since the CTA button doesn't actually navigate anywhere (href="#"),
// we'll focus on the actual user journey: reading about the process
// Wait for page to load
await page.waitForLoadState("networkidle");
// User starts by reading the hero section
await expect(page.locator("text=Collaborate")).toBeVisible();
await expect(
page.locator("text=Help your community make important decisions"),
).toBeVisible();
// User scrolls down to learn about how CommunityRule works
await page
.locator('h2:has-text("How CommunityRule works")')
.scrollIntoViewIfNeeded();
await expect(
page.locator('h2:has-text("How CommunityRule works")'),
).toBeVisible();
// User reads the process steps
await expect(
page.locator("text=Document how your community makes decisions"),
).toBeVisible();
await expect(
page.locator("text=Build an operating manual for a successful community"),
).toBeVisible();
await expect(
page.locator(
"text=Get a link to your manual for your group to review and evolve",
),
).toBeVisible();
// User explores rule templates
await page.locator("text=Consensus clusters").first().click();
await page.locator("text=Consensus").nth(1).click();
await page.locator("text=Elected Board").first().click();
await page.locator("text=Petition").first().click();
// User has successfully learned about how CommunityRule works
await expect(
page.locator("text=We've got your back, every step of the way"),
).toBeVisible();
});
test("user journey: scroll through entire page", async ({ page }) => {
// Start at top
await expect(page.locator("text=Collaborate")).toBeVisible();
// Simplified approach - just scroll to bottom and check footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await expect(page.locator("footer").first()).toBeVisible();
});
test("user journey: keyboard navigation through page", async ({ page }) => {
// Start with tab navigation
await page.keyboard.press("Tab");
await expect(page.locator(":focus")).toBeVisible();
// Navigate through all interactive elements
let tabCount = 0;
const maxTabs = 50; // Prevent infinite loop
while (tabCount < maxTabs) {
await page.keyboard.press("Tab");
tabCount++;
// Check if we've cycled back to the beginning
const focusedElement = page.locator(":focus");
if ((await focusedElement.count()) === 0) {
break;
}
}
// Test Enter key on focused elements
await page.keyboard.press("Enter");
});
test("user journey: mobile navigation", async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
// Navigate through page on mobile
await expect(page.locator("text=Collaborate")).toBeVisible();
// Scroll through sections
await page.locator("section").first().scrollIntoViewIfNeeded();
// Test basic touch interactions - check if elements exist and are visible first
const learnButton = page
.locator('button:has-text("Learn how CommunityRule works")')
.first();
if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) {
await learnButton.click();
}
const consensusText = page.locator("text=Consensus clusters");
if (
(await consensusText.count()) > 0 &&
(await consensusText.isVisible())
) {
await consensusText.click();
}
const askButton = page
.locator('button:has-text("Ask an organizer")')
.first();
if ((await askButton.count()) > 0 && (await askButton.isVisible())) {
await askButton.click();
}
});
test("user journey: tablet navigation", async ({ page }) => {
// Set tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
// Navigate through page on tablet
await expect(page.locator("text=Collaborate")).toBeVisible();
// Test tablet-specific interactions - check if elements exist and are visible first
const learnButton = page
.locator('button:has-text("Learn how CommunityRule works")')
.first();
if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) {
await learnButton.click();
}
const consensusText = page.locator("text=Consensus clusters");
if (
(await consensusText.count()) > 0 &&
(await consensusText.isVisible())
) {
await consensusText.click();
}
const askButton = page
.locator('button:has-text("Ask an organizer")')
.first();
if ((await askButton.count()) > 0 && (await askButton.isVisible())) {
await askButton.click();
}
});
test("user journey: desktop navigation", async ({ page }) => {
// Set desktop viewport
await page.setViewportSize({ width: 1440, height: 900 });
// Navigate through page on desktop
await expect(page.locator("text=Collaborate")).toBeVisible();
// Test desktop-specific interactions - check if elements exist and are visible first
const learnButton = page
.locator('button:has-text("Learn how CommunityRule works")')
.first();
if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) {
await learnButton.click();
}
const consensusText = page.locator("text=Consensus clusters");
if (
(await consensusText.count()) > 0 &&
(await consensusText.isVisible())
) {
await consensusText.click();
}
const askButton = page
.locator('button:has-text("Ask an organizer")')
.first();
if ((await askButton.count()) > 0 && (await askButton.isVisible())) {
await askButton.click();
}
});
test("user journey: accessibility navigation", async ({ page }) => {
// Test screen reader navigation
await page.keyboard.press("Tab");
// Navigate through landmarks
await page.keyboard.press("Tab");
await page.keyboard.press("Tab");
// Test heading navigation (if supported)
await page.keyboard.press("Tab");
// Test form navigation
await page.keyboard.press("Tab");
// Test button activation
await page.keyboard.press("Enter");
});
test("user journey: performance testing", async ({ page }) => {
// Measure initial page load
const startTime = Date.now();
await page.goto("/");
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000); // Should load within 3 seconds
// Measure scroll performance
const scrollStartTime = Date.now();
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
const scrollTime = Date.now() - scrollStartTime;
expect(scrollTime).toBeLessThan(1000); // Should scroll smoothly
// Measure interaction response time
const clickStartTime = Date.now();
const learnButton = page
.locator('button:has-text("Learn how CommunityRule works")')
.first();
if ((await learnButton.count()) > 0 && (await learnButton.isVisible())) {
await learnButton.click();
}
const clickTime = Date.now() - clickStartTime;
expect(clickTime).toBeLessThan(500); // Should respond quickly
});
});
+87 -377
View File
@@ -8,7 +8,8 @@ test.describe("Visual Regression Tests", () => {
});
await page.waitForTimeout(50);
}
test.beforeEach(async ({ page }) => {
test("homepage full page screenshot", async ({ page }) => {
// Add deterministic CSS to normalize rendering
await page.addStyleTag({
content: `
@@ -28,7 +29,6 @@ test.describe("Visual Regression Tests", () => {
});
await page.goto("/");
// Wait for all content to load
await page.waitForLoadState("networkidle");
// Make sure we've really got the webfonts before shots
@@ -39,9 +39,7 @@ test.describe("Visual Regression Tests", () => {
await document.fonts.ready;
}
});
});
test("homepage full page screenshot", async ({ page }) => {
// Stabilize layout before screenshot
await settle(page);
@@ -54,13 +52,42 @@ test.describe("Visual Regression Tests", () => {
});
test("homepage viewport screenshot", async ({ page }) => {
// Add deterministic CSS to normalize rendering
await page.addStyleTag({
content: `
/* stop caret and selection flicker */
* { caret-color: transparent !important; }
::selection { background: transparent !important; }
/* hide scrollbars */
::-webkit-scrollbar { display: none !important; }
html { scrollbar-width: none !important; }
/* stabilize font rasterization */
* {
text-rendering: geometricPrecision !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
`,
});
await page.goto("/");
await page.waitForLoadState("networkidle");
// Make sure we've really got the webfonts before shots
await page.evaluate(async () => {
// @ts-ignore
if (document.fonts && document.fonts.status !== "loaded") {
// @ts-ignore
await document.fonts.ready;
}
});
// Stabilize layout before screenshot
await page.evaluate(() => {
window.scrollTo(0, 0);
// Force layout & a frame boundary
void document.body.getBoundingClientRect();
});
await page.waitForTimeout(50); // give the compositor one tick
await page.waitForTimeout(50);
// Take viewport screenshot
await expect(page).toHaveScreenshot("homepage-viewport.png", {
@@ -68,326 +95,30 @@ test.describe("Visual Regression Tests", () => {
});
});
test("hero banner section screenshot", async ({ page }) => {
// Scroll to hero section and take screenshot
await page.locator("text=Collaborate").scrollIntoViewIfNeeded();
await page.waitForTimeout(500); // Wait for animations
// Stabilize layout before screenshot
await page.evaluate(() => {
// Force layout & a frame boundary
void document.body.getBoundingClientRect();
});
await page.waitForTimeout(50); // give the compositor one tick
const heroSection = page.locator("section").first();
await expect(heroSection).toHaveScreenshot("hero-banner.png", {
animations: "disabled",
});
});
test("logo wall section screenshot", async ({ page }) => {
// Scroll to logo wall section
await page
.locator("text=Trusted by leading cooperators")
.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const logoSection = page.locator("section").nth(1);
await expect(logoSection).toHaveScreenshot("logo-wall.png", {
animations: "disabled",
});
});
test("numbered cards section screenshot", async ({ page }) => {
// Scroll to numbered cards section
await page
.locator('h2:has-text("How CommunityRule works")')
.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const cardsSection = page.locator("section").nth(2);
await expect(cardsSection).toHaveScreenshot("numbered-cards.png", {
animations: "disabled",
});
});
test("rule stack section screenshot", async ({ page }) => {
// Scroll to rule stack section
await page.locator("text=Consensus clusters").scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const ruleSection = page.locator("section").nth(3);
await expect(ruleSection).toHaveScreenshot("rule-stack.png", {
animations: "disabled",
});
});
test("feature grid section screenshot", async ({ page }) => {
// Scroll to feature grid section - use a more reliable selector
await page.locator("text=We've got your back").scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const featureSection = page.locator("section").nth(4);
await expect(featureSection).toHaveScreenshot("feature-grid.png", {
animations: "disabled",
});
});
test("quote block section screenshot", async ({ page }) => {
// Scroll to quote block section
await page.locator("text=Jo Freeman").scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const quoteSection = page.locator("section").nth(5);
await expect(quoteSection).toHaveScreenshot("quote-block.png", {
animations: "disabled",
});
});
test("ask organizer section screenshot", async ({ page }) => {
// Scroll to ask organizer section
await page.locator("text=Still have questions?").scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const askSection = page.locator("section").nth(6);
await expect(askSection).toHaveScreenshot("ask-organizer.png", {
animations: "disabled",
});
});
test("header component screenshot", async ({ page }) => {
const header = page.locator("header");
await expect(header).toHaveScreenshot("header.png", {
animations: "disabled",
});
});
test("footer component screenshot", async ({ page }) => {
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Use a more specific selector for the main footer
const footer = page.locator("footer").last();
await expect(footer).toHaveScreenshot("footer.png", {
animations: "disabled",
});
});
test("mobile viewport screenshots", async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.waitForTimeout(1000);
// Wait for page to be stable
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot("homepage-mobile.png", {
animations: "disabled",
});
// Test mobile hero section - use a more reliable selector
const heroSection = page.locator("section").first();
if ((await heroSection.count()) > 0) {
await heroSection.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await expect(heroSection).toHaveScreenshot("hero-banner-mobile.png", {
animations: "disabled",
});
}
});
test("tablet viewport screenshots", async ({ page }) => {
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await page.waitForTimeout(1000);
// Wait for page to be stable
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot("homepage-tablet.png", {
animations: "disabled",
});
// Test tablet hero section - use a more reliable selector
const heroSection = page.locator("section").first();
if ((await heroSection.count()) > 0) {
await heroSection.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
await expect(heroSection).toHaveScreenshot("hero-banner-tablet.png", {
animations: "disabled",
});
}
});
test("desktop viewport screenshots", async ({ page }) => {
// Test desktop viewport
await page.setViewportSize({ width: 1440, height: 900 });
await page.waitForTimeout(1000);
await expect(page).toHaveScreenshot("homepage-desktop.png", {
animations: "disabled",
});
// Test desktop hero section
await page.locator("text=Collaborate").scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const heroSection = page.locator("section").first();
await expect(heroSection).toHaveScreenshot("hero-banner-desktop.png", {
animations: "disabled",
});
});
test("large desktop viewport screenshots", async ({ page }) => {
// Test large desktop viewport
await page.setViewportSize({ width: 1920, height: 1080 });
await page.waitForTimeout(1000);
await expect(page).toHaveScreenshot("homepage-large-desktop.png", {
animations: "disabled",
});
});
// test('button hover states', async ({ page }) => {
// // Test button hover states - scroll to hero section first to ensure button is visible
// await page.locator('text=Collaborate').scrollIntoViewIfNeeded();
// await page.waitForTimeout(500);
//
// // Use a more specific selector for the visible button
// const ctaButton = page.locator('button:has-text("Learn how CommunityRule works")').first();
//
// // Ensure button is visible
// await ctaButton.scrollIntoViewIfNeeded();
// await page.waitForTimeout(500);
//
// // Normal state
// await expect(ctaButton).toHaveScreenshot('button-normal.png', {
// animations: 'disabled'
// });
//
// // Hover state
// await ctaButton.hover();
// await page.waitForTimeout(500);
// await expect(ctaButton).toHaveScreenshot('button-hover.png', {
// animations: 'disabled'
// });
// });
test("rule card hover states", async ({ page }) => {
// Scroll to rule stack section
await page.locator("text=Consensus clusters").scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const consensusCard = page.locator('[aria-label*="Consensus clusters"]');
// Normal state
await expect(consensusCard).toHaveScreenshot("rule-card-normal.png", {
animations: "disabled",
});
// Hover state
await consensusCard.hover();
await page.waitForTimeout(500);
await expect(consensusCard).toHaveScreenshot("rule-card-hover.png", {
animations: "disabled",
});
});
test("feature card hover states", async ({ page }) => {
// Scroll to feature grid section
await page.locator("text=We've got your back").scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const featureCard = page.locator('a[href="#decision-making"]');
// Normal state
await expect(featureCard).toHaveScreenshot("feature-card-normal.png", {
animations: "disabled",
});
// Hover state
await featureCard.hover();
await page.waitForTimeout(500);
await expect(featureCard).toHaveScreenshot("feature-card-hover.png", {
animations: "disabled",
});
});
test("logo hover states", async ({ page }) => {
// Scroll to logo wall section
await page
.locator("text=Trusted by leading cooperators")
.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
const logo = page.locator('img[alt="Food Not Bombs"]');
// Normal state
await expect(logo).toHaveScreenshot("logo-normal.png", {
animations: "disabled",
});
// Hover state
await logo.hover();
await page.waitForTimeout(500);
await expect(logo).toHaveScreenshot("logo-hover.png", {
animations: "disabled",
});
});
// test('focus states', async ({ page }) => {
// // Test focus states for interactive elements - scroll to hero section first
// await page.locator('text=Collaborate').scrollIntoViewIfNeeded();
// await page.waitForTimeout(500);
//
// // Use first button and ensure it's visible
// const ctaButton = page.locator('button:has-text("Learn how CommunityRule works")').first();
//
// // Ensure button is visible
// await ctaButton.scrollIntoViewIfNeeded();
// await page.waitForTimeout(500);
//
// // Focus the button
// await ctaButton.focus();
// await page.waitForTimeout(500);
//
// await expect(ctaButton).toHaveScreenshot('button-focus.png', {
// animations: 'disabled'
// });
// });
test("loading states", async ({ page }) => {
// Test loading states by blocking resources
await page.route("**/*", (route) => {
// Delay all requests to simulate loading
setTimeout(() => route.continue(), 1000);
});
// Reload page to trigger loading states
await page.reload();
// Take screenshot during loading
await expect(page).toHaveScreenshot("homepage-loading.png", {
animations: "disabled",
});
});
test("blog listing page", async ({ page }) => {
// Add deterministic CSS to normalize rendering
await page.addStyleTag({
content: `
/* stop caret and selection flicker */
* { caret-color: transparent !important; }
::selection { background: transparent !important; }
/* hide scrollbars */
::-webkit-scrollbar { display: none !important; }
html { scrollbar-width: none !important; }
/* stabilize font rasterization */
* {
text-rendering: geometricPrecision !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
`,
});
// Navigate to blog listing page
await page.goto("/blog");
await page.waitForLoadState("networkidle");
// Wait for blog content to be fully rendered
await page.waitForSelector(
".grid.grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-3",
{ timeout: 10000 },
);
// Additional wait for any dynamic content to render
await page.waitForTimeout(1000);
await settle(page);
@@ -399,14 +130,30 @@ test.describe("Visual Regression Tests", () => {
});
test("blog post page", async ({ page }) => {
// Add deterministic CSS to normalize rendering
await page.addStyleTag({
content: `
/* stop caret and selection flicker */
* { caret-color: transparent !important; }
::selection { background: transparent !important; }
/* hide scrollbars */
::-webkit-scrollbar { display: none !important; }
html { scrollbar-width: none !important; }
/* stabilize font rasterization */
* {
text-rendering: geometricPrecision !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
`,
});
// Navigate to a specific blog post
await page.goto("/blog/resolving-active-conflicts");
await page.waitForLoadState("networkidle");
// Wait for blog post content to be fully rendered
await page.waitForSelector("main", { timeout: 10000 });
// Additional wait for any dynamic content to render
await page.waitForTimeout(1000);
await settle(page);
@@ -418,6 +165,24 @@ test.describe("Visual Regression Tests", () => {
});
test("404 error page", async ({ page }) => {
// Add deterministic CSS to normalize rendering
await page.addStyleTag({
content: `
/* stop caret and selection flicker */
* { caret-color: transparent !important; }
::selection { background: transparent !important; }
/* hide scrollbars */
::-webkit-scrollbar { display: none !important; }
html { scrollbar-width: none !important; }
/* stabilize font rasterization */
* {
text-rendering: geometricPrecision !important;
-webkit-font-smoothing: antialiased !important;
-moz-osx-font-smoothing: grayscale !important;
}
`,
});
// Navigate to a non-existent route to trigger 404
await page.goto("/non-existent-page");
await page.waitForLoadState("networkidle");
@@ -428,59 +193,4 @@ test.describe("Visual Regression Tests", () => {
animations: "disabled",
});
});
test("high contrast mode", async ({ page }) => {
// Simulate high contrast mode
await page.evaluate(() => {
document.body.style.filter = "contrast(200%)";
});
await expect(page).toHaveScreenshot("homepage-high-contrast.png", {
animations: "disabled",
});
// Reset contrast
await page.evaluate(() => {
document.body.style.filter = "none";
});
});
test("reduced motion mode", async ({ page }) => {
// Simulate reduced motion preference
await page.evaluate(() => {
document.documentElement.style.setProperty(
"--prefers-reduced-motion",
"reduce",
);
});
await expect(page).toHaveScreenshot("homepage-reduced-motion.png", {
animations: "disabled",
});
});
test("dark mode simulation", async ({ page }) => {
// Navigate to homepage first
await page.goto("/");
await page.waitForLoadState("networkidle");
await settle(page);
// Simulate dark mode (if supported)
await page.evaluate(() => {
document.documentElement.classList.add("dark");
document.body.style.backgroundColor = "#000";
document.body.style.color = "#fff";
});
await expect(page).toHaveScreenshot("homepage-dark-mode.png", {
animations: "disabled",
});
// Reset to light mode
await page.evaluate(() => {
document.documentElement.classList.remove("dark");
document.body.style.backgroundColor = "";
document.body.style.color = "";
});
});
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Some files were not shown because too many files have changed in this diff Show More