Give every public route its own description, and keep canonicals and structured data on the host that serves the page.
Decorative images on the marketing pages stay quiet, and every dialog close button is named Close. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
0b6e8b00dd
commit
c180e79c38
52 files changed
+363
-209
No files matched your search
@@ -36,7 +36,7 @@ describe("Alert dismiss control", () => {
|
||||
it("omits close button when onClose is absent", () => {
|
||||
render(<Alert title="T" description="D" />);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Close alert" }),
|
||||
screen.queryByRole("button", { name: "Close" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("Alert dismiss control", () => {
|
||||
const user = userEvent.setup();
|
||||
const onClose = vi.fn();
|
||||
render(<Alert title="T" onClose={onClose} />);
|
||||
await user.click(screen.getByRole("button", { name: "Close alert" }));
|
||||
await user.click(screen.getByRole("button", { name: "Close" }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("Alert dismiss control", () => {
|
||||
<Alert title="T" onClose={onClose} hasTrailingIcon={false} />,
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Close alert" }),
|
||||
screen.queryByRole("button", { name: "Close" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe("ConfirmStakeholdersScreen", () => {
|
||||
expect(
|
||||
screen.getByText(/Congratulations! You've drafted your CommunityRule!/i),
|
||||
).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Close alert" }));
|
||||
await user.click(screen.getByRole("button", { name: "Close" }));
|
||||
expect(
|
||||
screen.queryByText(
|
||||
/Congratulations! You've drafted your CommunityRule!/i,
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("Create", () => {
|
||||
it("calls onClose when close button is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
renderWithProviders(<Create {...defaultProps} onClose={onClose} />);
|
||||
const closeButton = screen.getByLabelText("Close dialog");
|
||||
const closeButton = screen.getByLabelText("Close");
|
||||
fireEvent.click(closeButton);
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -269,7 +269,7 @@ describe("Create", () => {
|
||||
</Create>,
|
||||
);
|
||||
|
||||
const closeButton = screen.getByLabelText("Close dialog");
|
||||
const closeButton = screen.getByLabelText("Close");
|
||||
screen.getByLabelText("Test Input"); // Verify input is rendered
|
||||
|
||||
// Focus should start on first focusable element (close button)
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("Dialog", () => {
|
||||
it("calls onClose when close control is activated", () => {
|
||||
const onClose = vi.fn();
|
||||
renderWithProviders(<Dialog {...defaultProps} onClose={onClose} />);
|
||||
fireEvent.click(screen.getByLabelText("Close dialog"));
|
||||
fireEvent.click(screen.getByLabelText("Close"));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -440,9 +440,9 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Save" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).queryByRole("button", { name: "Close" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(within(dialog).getAllByRole("button", { name: "Close" })).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("opens Save for a values chip (use-without-changes seeded snapshot)", async () => {
|
||||
@@ -579,7 +579,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
expect(
|
||||
within(dialog).queryByRole("button", { name: "Back" }),
|
||||
).not.toBeInTheDocument();
|
||||
fireEvent.click(within(dialog).getByLabelText("Close dialog"));
|
||||
fireEvent.click(within(dialog).getByLabelText("Close"));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -92,7 +92,7 @@ describe("Login", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText("Close dialog"));
|
||||
fireEvent.click(screen.getByLabelText("Close"));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -169,7 +169,7 @@ describe("Login", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByLabelText("Close dialog")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Close")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("More options")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -31,14 +31,14 @@ componentTestSuite<LogoProps>(config);
|
||||
describe("Logo (behavioral tests)", () => {
|
||||
it("renders as a link to home", () => {
|
||||
render(<Logo />);
|
||||
const logo = screen.getByRole("link", { name: /communityrule logo/i });
|
||||
const logo = screen.getByRole("link", { name: "CommunityRule" });
|
||||
expect(logo).toHaveAttribute("href", "/");
|
||||
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
|
||||
expect(logo).toHaveAttribute("aria-label", "CommunityRule");
|
||||
});
|
||||
|
||||
it("animates the mark on hover without scaling the wordmark", () => {
|
||||
render(<Logo />);
|
||||
const logo = screen.getByRole("link", { name: /communityrule logo/i });
|
||||
const logo = screen.getByRole("link", { name: "CommunityRule" });
|
||||
const wordmark = screen.getByText("CommunityRule");
|
||||
const mark = logo.querySelector("img");
|
||||
expect(logo).toHaveClass("group");
|
||||
@@ -48,7 +48,7 @@ describe("Logo (behavioral tests)", () => {
|
||||
|
||||
it("renders logo icon", () => {
|
||||
render(<Logo />);
|
||||
expect(screen.getByAltText("CommunityRule Logo")).toBeInTheDocument();
|
||||
expect(screen.getByAltText("CommunityRule")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders text by default", () => {
|
||||
@@ -60,7 +60,7 @@ describe("Logo (behavioral tests)", () => {
|
||||
const { container } = render(<Logo wordmark={false} />);
|
||||
const textElement = container.querySelector(".hidden");
|
||||
expect(textElement).toBeInTheDocument();
|
||||
expect(screen.getByAltText("CommunityRule Logo")).toBeInTheDocument();
|
||||
expect(screen.getByAltText("CommunityRule")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies inverse palette styling when palette is inverse", () => {
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import SectionNumber from "../../app/components/sections/SectionNumber";
|
||||
|
||||
describe("SectionNumber", () => {
|
||||
it("rotates the shape and keeps the numeral upright", () => {
|
||||
render(<SectionNumber number={1} />);
|
||||
const { container } = render(<SectionNumber number={1} />);
|
||||
|
||||
const numeral = screen.getByText("1");
|
||||
const numeral = container.querySelector("span");
|
||||
expect(numeral).toHaveTextContent("1");
|
||||
expect(numeral).toHaveClass("text-large-label");
|
||||
expect(numeral.className).not.toMatch(/rotate/);
|
||||
expect(numeral?.className).not.toMatch(/rotate/);
|
||||
|
||||
const shape = screen.getByAltText("Section 1");
|
||||
const shape = container.querySelector("img");
|
||||
expect(shape).toHaveAttribute("alt", "");
|
||||
expect(shape).toHaveClass("rotate-15");
|
||||
expect(shape.parentElement).not.toHaveClass("rotate-15");
|
||||
expect(shape.parentElement?.className).not.toMatch(/-rotate-/);
|
||||
expect(shape?.parentElement).toHaveAttribute("aria-hidden", "true");
|
||||
expect(shape?.parentElement).not.toHaveClass("rotate-15");
|
||||
expect(shape?.parentElement?.className).not.toMatch(/-rotate-/);
|
||||
});
|
||||
});
|
||||
@@ -143,7 +143,7 @@ test.describe("Critical User Journeys", () => {
|
||||
await expect(page.locator("header")).toBeVisible();
|
||||
|
||||
// Test logo click
|
||||
const logoLinks = page.locator('a[aria-label="CommunityRule Logo"]');
|
||||
const logoLinks = page.locator('a[aria-label="CommunityRule"]');
|
||||
const logoCount = await logoLinks.count();
|
||||
expect(logoCount).toBeGreaterThan(0);
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ describe("Create flow communication-methods page", () => {
|
||||
expect(within(dialog).getByText("Guidelines")).toBeInTheDocument();
|
||||
expect(within(dialog).getByRole("textbox")).toHaveValue("Enter norms here");
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", { name: "Close dialog" }),
|
||||
within(dialog).getByRole("button", { name: "Close" }),
|
||||
);
|
||||
|
||||
const metaAfterClose = latest.customMethodCardMetaById ?? {};
|
||||
|
||||
@@ -483,7 +483,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
).not.toBeChecked();
|
||||
await user.click(
|
||||
within(doocracyDialog).getByRole("button", { name: "Close dialog" }),
|
||||
within(doocracyDialog).getByRole("button", { name: "Close" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
@@ -541,7 +541,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
await user.click(
|
||||
within(doocracyDialog).getByRole("button", { name: "Close dialog" }),
|
||||
within(doocracyDialog).getByRole("button", { name: "Close" }),
|
||||
);
|
||||
await user.click(await screen.findByRole("button", { name: "Discard" }));
|
||||
await waitFor(() => {
|
||||
@@ -639,7 +639,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
await user.click(
|
||||
within(lazyAgain).getByRole("button", { name: "Close dialog" }),
|
||||
within(lazyAgain).getByRole("button", { name: "Close" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
|
||||
@@ -26,6 +26,13 @@ vi.mock("../../lib/assetUtils", () => ({
|
||||
}));
|
||||
|
||||
// Mock blog post data
|
||||
function decorativeIcon() {
|
||||
const icon = document.querySelector("img");
|
||||
expect(icon).toBeTruthy();
|
||||
expect(icon).toHaveAttribute("alt", "");
|
||||
return icon;
|
||||
}
|
||||
|
||||
const mockPost = {
|
||||
slug: "test-article",
|
||||
frontmatter: {
|
||||
@@ -57,7 +64,7 @@ describe("ContentContainer", () => {
|
||||
it("displays the icon correctly", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
const icon = decorativeIcon();
|
||||
expect(icon).toBeInTheDocument();
|
||||
expect(icon).toHaveAttribute(
|
||||
"src",
|
||||
@@ -125,9 +132,7 @@ describe("ContentContainer", () => {
|
||||
it("has proper spacing between icon and text", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const iconContainer = screen
|
||||
.getByAltText("Icon for Test Article Title")
|
||||
.closest("div");
|
||||
const iconContainer = decorativeIcon().closest("div");
|
||||
const textContainer = screen.getByText("Test Article Title").closest("div");
|
||||
|
||||
// Check the content container (parent of icon)
|
||||
@@ -169,7 +174,7 @@ describe("ContentContainer", () => {
|
||||
it("uses per-article tag assets for catalog slugs", () => {
|
||||
const { rerender } = render(<ContentContainer post={mockPost} />);
|
||||
|
||||
let icon = screen.getByAltText("Icon for Test Article Title");
|
||||
let icon = decorativeIcon();
|
||||
expect(icon).toHaveAttribute(
|
||||
"src",
|
||||
"/content/blog/resolving-active-conflicts-tag.svg",
|
||||
@@ -178,7 +183,7 @@ describe("ContentContainer", () => {
|
||||
const post2 = { ...mockPost, slug: "operational-security-mutual-aid" };
|
||||
rerender(<ContentContainer post={post2} />);
|
||||
|
||||
icon = screen.getByAltText("Icon for Test Article Title");
|
||||
icon = decorativeIcon();
|
||||
expect(icon).toHaveAttribute(
|
||||
"src",
|
||||
"/content/blog/operational-security-mutual-aid-tag.svg",
|
||||
@@ -187,7 +192,7 @@ describe("ContentContainer", () => {
|
||||
const post3 = { ...mockPost, slug: "making-decisions-without-hierarchy" };
|
||||
rerender(<ContentContainer post={post3} />);
|
||||
|
||||
icon = screen.getByAltText("Icon for Test Article Title");
|
||||
icon = decorativeIcon();
|
||||
expect(icon).toHaveAttribute(
|
||||
"src",
|
||||
"/content/blog/making-decisions-without-hierarchy-tag.svg",
|
||||
@@ -211,7 +216,7 @@ describe("ContentContainer", () => {
|
||||
it("applies correct responsive sizing for xs breakpoint", () => {
|
||||
render(<ContentContainer post={mockPost} size="xs" />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
const icon = decorativeIcon();
|
||||
expect(icon).toHaveClass("w-[60px]", "h-[30px]");
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
@@ -224,7 +229,7 @@ describe("ContentContainer", () => {
|
||||
it("applies correct responsive sizing for responsive breakpoint", () => {
|
||||
render(<ContentContainer post={mockPost} size="responsive" />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
const icon = decorativeIcon();
|
||||
expect(icon).toHaveClass("w-[60px]", "h-[30px]");
|
||||
|
||||
const title = screen.getByText("Test Article Title");
|
||||
@@ -237,8 +242,8 @@ describe("ContentContainer", () => {
|
||||
it("has proper accessibility attributes", () => {
|
||||
render(<ContentContainer post={mockPost} />);
|
||||
|
||||
const icon = screen.getByAltText("Icon for Test Article Title");
|
||||
expect(icon).toHaveAttribute("alt", "Icon for Test Article Title");
|
||||
const icon = decorativeIcon();
|
||||
expect(icon).toHaveAttribute("alt", "");
|
||||
});
|
||||
|
||||
it("handles long titles gracefully", () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("QuoteBlock Component", () => {
|
||||
expect(
|
||||
screen.getByText("The Tyranny of Structurelessness"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByAltText("Portrait of Jo Freeman")).toBeInTheDocument();
|
||||
expect(document.querySelector("img")).toHaveAttribute("alt", "");
|
||||
});
|
||||
|
||||
test("renders with custom props", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
GOVERNANCE_TEMPLATE_CATALOG,
|
||||
GOVERNANCE_TEMPLATE_HOME_SLUGS,
|
||||
getGovernanceTemplatesForHome,
|
||||
templateReviewDocument,
|
||||
} from "../../lib/templates/governanceTemplateCatalog";
|
||||
|
||||
/**
|
||||
@@ -48,4 +49,19 @@ describe("governanceTemplateCatalog (Figma 21764-16435)", () => {
|
||||
]);
|
||||
expect(getGovernanceTemplatesForHome()).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("puts the template name in the review document title", () => {
|
||||
expect(templateReviewDocument("consensus", null)).toEqual({
|
||||
title: "Consensus",
|
||||
description:
|
||||
"Important decisions require unanimous agreement. Proposals pass only if no serious objections remain.",
|
||||
});
|
||||
expect(
|
||||
templateReviewDocument("consensus", {
|
||||
title: "Saved Consensus",
|
||||
description: "Saved description",
|
||||
})?.title,
|
||||
).toBe("Saved Consensus");
|
||||
expect(templateReviewDocument("missing-slug", null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -24,16 +24,12 @@ describe("useSchemaData", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("generates WebSite schema", () => {
|
||||
test("generates WebSite schema without a search action", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSchemaData({
|
||||
type: "WebSite",
|
||||
name: "Test Site",
|
||||
url: "https://example.com",
|
||||
potentialAction: {
|
||||
target: "https://example.com/search?q={search_term_string}",
|
||||
"query-input": "required name=search_term_string",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -42,12 +38,8 @@ describe("useSchemaData", () => {
|
||||
"@type": "WebSite",
|
||||
name: "Test Site",
|
||||
url: "https://example.com",
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: "https://example.com/search?q={search_term_string}",
|
||||
"query-input": "required name=search_term_string",
|
||||
},
|
||||
});
|
||||
expect(result.current).not.toHaveProperty("potentialAction");
|
||||
});
|
||||
|
||||
test("generates HowTo schema", () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import metadataMessages from "../../messages/en/metadata.json";
|
||||
import {
|
||||
absoluteSiteUrl,
|
||||
articleShareImages,
|
||||
buildRobots,
|
||||
configuredSiteOrigin,
|
||||
metadataBaseUrl,
|
||||
NO_INDEX_ROBOTS,
|
||||
originFromHost,
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
routeMetadata,
|
||||
sitemapUrl,
|
||||
templatesShareImages,
|
||||
websiteJsonLd,
|
||||
} from "../../lib/siteMetadata";
|
||||
|
||||
const ORIGINAL_SITE_INDEXING = process.env.SITE_INDEXING;
|
||||
@@ -66,6 +69,9 @@ describe("routeMetadata", () => {
|
||||
});
|
||||
expect(meta.title).toBe("Learn");
|
||||
expect(meta.description).toBe("Copy");
|
||||
expect(meta.openGraph?.description).toBe("Copy");
|
||||
expect(meta.openGraph?.title).toBe("Learn");
|
||||
expect(meta.twitter?.description).toBe("Copy");
|
||||
expect(meta.alternates?.canonical).toBe("/learn");
|
||||
expect(meta.openGraph?.url).toBe("/learn");
|
||||
});
|
||||
@@ -99,6 +105,32 @@ describe("routeMetadata", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("absoluteSiteUrl", () => {
|
||||
it("uses the configured origin, which is production when no app origin is set", () => {
|
||||
delete process.env.CLOUDRON_APP_ORIGIN;
|
||||
expect(PRODUCTION_SITE_ORIGIN).toBe("https://communityrule.info");
|
||||
expect(configuredSiteOrigin()).toBe(PRODUCTION_SITE_ORIGIN);
|
||||
expect(absoluteSiteUrl("/")).toBe("https://communityrule.info");
|
||||
expect(absoluteSiteUrl("/about")).toBe("https://communityrule.info/about");
|
||||
});
|
||||
|
||||
it("follows CLOUDRON_APP_ORIGIN so a staging build does not advertise production", () => {
|
||||
process.env.CLOUDRON_APP_ORIGIN = "https://staging.communityrule.info";
|
||||
expect(absoluteSiteUrl("/learn")).toBe(
|
||||
"https://staging.communityrule.info/learn",
|
||||
);
|
||||
});
|
||||
|
||||
it("builds a WebSite block with no search action", () => {
|
||||
expect(websiteJsonLd("https://communityrule.info")).toEqual({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: "CommunityRule",
|
||||
url: "https://communityrule.info",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sitemapUrl", () => {
|
||||
it("resolves paths against the production origin", () => {
|
||||
expect(sitemapUrl("/")).toBe(PRODUCTION_SITE_ORIGIN);
|
||||
@@ -137,7 +169,8 @@ describe("originFromHost", () => {
|
||||
expect(originFromHost("staging.communityrule.info")).toBe(
|
||||
"https://staging.communityrule.info",
|
||||
);
|
||||
expect(originFromHost("communityrule.com")).toBe(PRODUCTION_SITE_ORIGIN);
|
||||
expect(originFromHost("communityrule.info")).toBe(PRODUCTION_SITE_ORIGIN);
|
||||
expect(originFromHost("communityrule.com")).toBe("https://communityrule.com");
|
||||
});
|
||||
|
||||
it("keeps http for localhost so local previews resolve", () => {
|
||||
|
||||
Reference in new issue
Block a user