ab806fbc16
CI Pipeline / test (20) (pull_request) Successful in 6m28s
CI Pipeline / test (18) (pull_request) Successful in 8m20s
CI Pipeline / e2e (firefox) (pull_request) Successful in 3m15s
CI Pipeline / e2e (webkit) (pull_request) Successful in 3m39s
CI Pipeline / e2e (chromium) (pull_request) Successful in 11m5s
CI Pipeline / visual-regression (pull_request) Successful in 6m4s
CI Pipeline / storybook (pull_request) Successful in 2m27s
CI Pipeline / build (pull_request) Successful in 2m29s
CI Pipeline / performance (pull_request) Successful in 4m54s
337 lines
10 KiB
React
337 lines
10 KiB
React
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { vi, describe, test, expect, afterEach } from "vitest";
|
|
import React from "react";
|
|
import Page from "../../app/page";
|
|
|
|
// Mock next/dynamic to return components synchronously in tests
|
|
vi.mock("next/dynamic", () => {
|
|
return {
|
|
default: (importFn, options) => {
|
|
// In tests, resolve the dynamic import immediately and return the component
|
|
let Component = null;
|
|
importFn().then((mod) => {
|
|
Component = mod.default || mod;
|
|
});
|
|
// Return a synchronous wrapper that uses the mocked component
|
|
return (props) => {
|
|
// Use the mocked component directly
|
|
if (Component) {
|
|
return <Component {...props} />;
|
|
}
|
|
// Fallback: return the loading placeholder if component not ready
|
|
return options?.loading ? options.loading() : null;
|
|
};
|
|
},
|
|
};
|
|
});
|
|
import Header from "../../app/components/Header";
|
|
import Footer from "../../app/components/Footer";
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
describe("User Journey Integration", () => {
|
|
// TODO: Fix next/dynamic mock to properly handle async component loading
|
|
test.skip("new user discovers the application through hero section", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<Header />
|
|
<Page />
|
|
<Footer />
|
|
</div>,
|
|
);
|
|
|
|
// User sees the main value proposition
|
|
expect(
|
|
screen.getByText(/Help your community make important decisions/),
|
|
).toBeInTheDocument();
|
|
|
|
// User clicks the main CTA to learn more
|
|
const learnButtons = screen.getAllByRole("button", {
|
|
name: "Learn how CommunityRule works",
|
|
});
|
|
const learnButton = learnButtons[0];
|
|
await user.click(learnButton);
|
|
|
|
// Wait for dynamically imported NumberedCards component
|
|
await waitFor(() => {
|
|
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// TODO: Fix next/dynamic mock to properly handle async component loading
|
|
test.skip("user explores different governance types", async () => {
|
|
const user = userEvent.setup();
|
|
render(<Page />);
|
|
|
|
// Wait for dynamically imported RuleStack component
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
|
|
});
|
|
expect(screen.getByText("Elected Board")).toBeInTheDocument();
|
|
expect(screen.getByText("Consensus")).toBeInTheDocument();
|
|
expect(screen.getByText("Petition")).toBeInTheDocument();
|
|
|
|
// User clicks on a governance type to create a rule
|
|
const createButtons = screen.getAllByRole("button", {
|
|
name: "Create CommunityRule",
|
|
});
|
|
expect(createButtons.length).toBeGreaterThan(0);
|
|
|
|
await user.click(createButtons[0]);
|
|
// Button should remain interactive
|
|
expect(createButtons[0]).toBeInTheDocument();
|
|
});
|
|
|
|
test("user navigates through the application using header navigation", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<Header />
|
|
<Page />
|
|
<Footer />
|
|
</div>,
|
|
);
|
|
|
|
// User clicks on navigation links in header (check that they exist and are clickable)
|
|
const navigationLinks = screen.getAllByRole("link");
|
|
const headerNavLinks = navigationLinks.filter(
|
|
(link) =>
|
|
link.textContent?.includes("Use Cases") ||
|
|
link.textContent?.includes("Learn") ||
|
|
link.textContent?.includes("About"),
|
|
);
|
|
|
|
// Test that navigation links are present and clickable
|
|
for (const link of headerNavLinks) {
|
|
await user.click(link);
|
|
expect(link).toBeInTheDocument();
|
|
}
|
|
});
|
|
|
|
test("user seeks help through ask organizer section", async () => {
|
|
const user = userEvent.setup();
|
|
render(<Page />);
|
|
|
|
// User scrolls to the bottom and sees the help section
|
|
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText("Get answers from an experienced organizer"),
|
|
).toBeInTheDocument();
|
|
|
|
// User clicks the ask organizer button (it's actually a link, not a button)
|
|
const askLink = screen.getByRole("link", { name: /Ask an organizer/i });
|
|
await user.click(askLink);
|
|
expect(askLink).toHaveAttribute("href", "#contact");
|
|
});
|
|
|
|
test("user explores the process through numbered cards", async () => {
|
|
const user = userEvent.setup();
|
|
render(<Page />);
|
|
|
|
// Wait for dynamically imported NumberedCards component
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText("Document how your community makes decisions"),
|
|
).toBeInTheDocument();
|
|
});
|
|
expect(
|
|
screen.getByText("Build an operating manual for a successful community"),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText(
|
|
"Get a link to your manual for your group to review and evolve",
|
|
),
|
|
).toBeInTheDocument();
|
|
|
|
// User sees the step numbers
|
|
const stepNumbers = screen.getAllByText(/1|2|3/);
|
|
expect(stepNumbers.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("user accesses contact information through footer", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<Header />
|
|
<Page />
|
|
<Footer />
|
|
</div>,
|
|
);
|
|
|
|
// User finds contact email in footer
|
|
const emailLink = screen.getByRole("link", { name: "medlab@colorado.edu" });
|
|
expect(emailLink).toHaveAttribute("href", "mailto:medlab@colorado.edu");
|
|
|
|
// User finds social media links
|
|
const blueskyLink = screen.getByRole("link", {
|
|
name: "Follow us on Bluesky",
|
|
});
|
|
const gitlabLink = screen.getByRole("link", {
|
|
name: "Follow us on GitLab",
|
|
});
|
|
|
|
expect(blueskyLink).toBeInTheDocument();
|
|
expect(gitlabLink).toBeInTheDocument();
|
|
});
|
|
|
|
test("user explores features and benefits", async () => {
|
|
const user = userEvent.setup();
|
|
render(<Page />);
|
|
|
|
// Wait for dynamically imported FeatureGrid component
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText("We've got your back, every step of the way"),
|
|
).toBeInTheDocument();
|
|
});
|
|
expect(
|
|
screen.getByText(
|
|
"Use our toolkit to improve, document, and evolve your organization.",
|
|
),
|
|
).toBeInTheDocument();
|
|
|
|
// User sees the testimonial/quote (check for the actual quote content)
|
|
expect(
|
|
screen.getByText(/The rules of decision-making must be open/),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
test("user interacts with logo wall and social proof", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<Page />
|
|
<Footer />
|
|
</div>,
|
|
);
|
|
|
|
// Wait for dynamically imported LogoWall component
|
|
await waitFor(() => {
|
|
const logoImages = screen.getAllByRole("img");
|
|
const partnerLogos = logoImages.filter(
|
|
(img) =>
|
|
img.alt?.includes("Food Not Bombs") ||
|
|
img.alt?.includes("Start COOP") ||
|
|
img.alt?.includes("Metagov") ||
|
|
img.alt?.includes("Open Civics") ||
|
|
img.alt?.includes("Mutual Aid CO") ||
|
|
img.alt?.includes("CU Boulder"),
|
|
);
|
|
expect(partnerLogos.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// Social links should be present in footer
|
|
const blueskyLink = screen.getByRole("link", { name: /Bluesky/i });
|
|
const gitlabLink = screen.getByRole("link", { name: /GitLab/i });
|
|
expect(blueskyLink).toBeInTheDocument();
|
|
expect(gitlabLink).toBeInTheDocument();
|
|
});
|
|
|
|
test("user completes the full journey from discovery to action", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<Header />
|
|
<Page />
|
|
<Footer />
|
|
</div>,
|
|
);
|
|
|
|
// 1. User discovers the application
|
|
expect(screen.getByText("Collaborate")).toBeInTheDocument();
|
|
expect(screen.getByText("with clarity")).toBeInTheDocument();
|
|
|
|
// 2. User learns how it works - wait for dynamically imported component
|
|
await waitFor(() => {
|
|
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
|
|
});
|
|
|
|
// 3. User sees governance options - wait for dynamically imported component
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
|
|
});
|
|
|
|
// 4. User sees features and benefits - wait for dynamically imported component
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByText("We've got your back, every step of the way"),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
// 5. User sees social proof
|
|
expect(
|
|
screen.getByText(/The rules of decision-making must be open/),
|
|
).toBeInTheDocument();
|
|
|
|
// 6. User can take action
|
|
const createButtons = screen.getAllByRole("button", {
|
|
name: "Create CommunityRule",
|
|
});
|
|
expect(createButtons.length).toBeGreaterThan(0);
|
|
|
|
// 7. User can get help if needed
|
|
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
|
|
});
|
|
|
|
test("user can access all navigation options consistently", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<Header />
|
|
<Page />
|
|
<Footer />
|
|
</div>,
|
|
);
|
|
|
|
// Header navigation
|
|
const headerNav = screen.getByRole("navigation");
|
|
expect(headerNav).toBeInTheDocument();
|
|
|
|
// Footer navigation
|
|
const footerLinks = screen.getAllByRole("link");
|
|
const navigationLinks = footerLinks.filter(
|
|
(link) =>
|
|
link.textContent?.includes("Use cases") ||
|
|
link.textContent?.includes("Learn") ||
|
|
link.textContent?.includes("About"),
|
|
);
|
|
expect(navigationLinks.length).toBeGreaterThan(0);
|
|
|
|
// All navigation links should be accessible
|
|
navigationLinks.forEach((link) => {
|
|
expect(link).not.toHaveAttribute("tabindex", "-1");
|
|
});
|
|
});
|
|
|
|
test("user can complete actions without errors", async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<Header />
|
|
<Page />
|
|
<Footer />
|
|
</div>,
|
|
);
|
|
|
|
// Test all interactive elements
|
|
const buttons = screen.getAllByRole("button");
|
|
const links = screen.getAllByRole("link");
|
|
|
|
// All buttons should be clickable
|
|
for (const button of buttons) {
|
|
await user.click(button);
|
|
expect(button).toBeInTheDocument();
|
|
}
|
|
|
|
// All links should be accessible
|
|
for (const link of links) {
|
|
expect(link).toBeInTheDocument();
|
|
expect(link).not.toHaveAttribute("tabindex", "-1");
|
|
}
|
|
});
|
|
});
|